From eb25e25d534e1526c401accccc0732ecd39229b4 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 18:56:59 +0000 Subject: [PATCH 01/10] Added io.crowsetta module --- docs/reference/io.md | 2 + mkdocs.yml | 2 +- pdm.lock | 778 +++++++++++------- pyproject.toml | 19 +- src/soundevent/data/recordings.py | 21 + src/soundevent/io/__init__.py | 3 + src/soundevent/io/crowsetta/__init__.py | 41 + src/soundevent/io/crowsetta/annotation.py | 237 ++++++ src/soundevent/io/crowsetta/bbox.py | 181 ++++ src/soundevent/io/crowsetta/labels.py | 245 ++++++ src/soundevent/io/crowsetta/segment.py | 171 ++++ src/soundevent/io/crowsetta/sequence.py | 122 +++ tests/test_io/conftest.py | 24 + tests/test_io/test_crowsetta/__init__.py | 0 .../test_io/test_crowsetta/test_annotation.py | 318 +++++++ tests/test_io/test_crowsetta/test_bbox.py | 200 +++++ tests/test_io/test_crowsetta/test_labels.py | 266 ++++++ tests/test_io/test_crowsetta/test_segments.py | 160 ++++ tests/test_io/test_crowsetta/test_sequence.py | 172 ++++ 19 files changed, 2650 insertions(+), 312 deletions(-) create mode 100644 src/soundevent/io/crowsetta/__init__.py create mode 100644 src/soundevent/io/crowsetta/annotation.py create mode 100644 src/soundevent/io/crowsetta/bbox.py create mode 100644 src/soundevent/io/crowsetta/labels.py create mode 100644 src/soundevent/io/crowsetta/segment.py create mode 100644 src/soundevent/io/crowsetta/sequence.py create mode 100644 tests/test_io/test_crowsetta/__init__.py create mode 100644 tests/test_io/test_crowsetta/test_annotation.py create mode 100644 tests/test_io/test_crowsetta/test_bbox.py create mode 100644 tests/test_io/test_crowsetta/test_labels.py create mode 100644 tests/test_io/test_crowsetta/test_segments.py create mode 100644 tests/test_io/test_crowsetta/test_sequence.py diff --git a/docs/reference/io.md b/docs/reference/io.md index 0841f19..1e29cb4 100644 --- a/docs/reference/io.md +++ b/docs/reference/io.md @@ -7,3 +7,5 @@ - DataCollections - save - load + +::: soundevent.io.crowsetta diff --git a/mkdocs.yml b/mkdocs.yml index 087274e..577a141 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,7 +81,7 @@ plugins: docstring_section_style: "table" summary: true signature_crossrefs: true - show_signature_annotations: true + show_signature_annotations: false filters: - "!^_" docstring_options: diff --git a/pdm.lock b/pdm.lock index 08a15eb..21299ca 100644 --- a/pdm.lock +++ b/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "all", "audio", "dev", "evaluation", "geometry", "plot"] +groups = ["default", "all", "audio", "dev", "evaluation", "geometry", "plot", "crowsetta"] strategy = ["cross_platform"] -lock_version = "4.4" -content_hash = "sha256:979a03b717e82edb585c684de22566827b48011d5984f31fde415f2af99d81b1" +lock_version = "4.4.1" +content_hash = "sha256:1f8d4f2e93a0dce6ce636f709ee0b787a47611b8be7f7a81de3ab27f397f028a" [[package]] name = "annotated-types" @@ -20,6 +20,15 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] +[[package]] +name = "appdirs" +version = "1.4.4" +summary = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +files = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] + [[package]] name = "attrs" version = "23.2.0" @@ -43,9 +52,22 @@ files = [ {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, ] +[[package]] +name = "birdsong-recognition-dataset" +version = "0.3.2.post1" +requires_python = ">=3.8" +summary = "Functions for working with this data repository: https://figshare.com/articles/BirdsongRecognition/3470165" +dependencies = [ + "numpy>=1.18.1", +] +files = [ + {file = "birdsong-recognition-dataset-0.3.2.post1.tar.gz", hash = "sha256:7d3378b07623c0220543501aa9c8e52cb2602963ac23abe6ba9ec563cc3957f5"}, + {file = "birdsong_recognition_dataset-0.3.2.post1-py3-none-any.whl", hash = "sha256:72c4288b39a06b2253a50ee62f28202a0ff7765152183d8f0782d25484694e0c"}, +] + [[package]] name = "black" -version = "23.12.1" +version = "24.1.1" requires_python = ">=3.8" summary = "The uncompromising code formatter." dependencies = [ @@ -58,28 +80,28 @@ dependencies = [ "typing-extensions>=4.0.1; python_version < \"3.11\"", ] files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, - {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, - {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, - {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, + {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, + {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, + {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, + {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, + {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, + {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, + {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, + {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, + {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, + {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, + {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, + {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, + {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, + {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, + {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, + {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, + {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, + {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, + {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, + {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, + {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, + {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, ] [[package]] @@ -339,127 +361,149 @@ files = [ [[package]] name = "coverage" -version = "7.4.0" +version = "7.4.1" requires_python = ">=3.8" summary = "Code coverage measurement for Python" files = [ - {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, - {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, - {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, - {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, - {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, - {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, - {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, - {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, - {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, - {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, - {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, - {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, - {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, - {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, ] [[package]] name = "coverage" -version = "7.4.0" +version = "7.4.1" extras = ["toml"] requires_python = ">=3.8" summary = "Code coverage measurement for Python" dependencies = [ - "coverage==7.4.0", + "coverage==7.4.1", "tomli; python_full_version <= \"3.11.0a6\"", ] files = [ - {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, - {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, - {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, - {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, - {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, - {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, - {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, - {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, - {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, - {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, - {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, - {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, - {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, - {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, +] + +[[package]] +name = "crowsetta" +version = "4.0.0.post2" +requires_python = ">=3.8" +summary = "A Python tool to work with any format for annotating animal vocalizations and bioacoustics data" +dependencies = [ + "SoundFile>=0.10.3", + "appdirs>=1.4.4", + "attrs>=19.3.0", + "birdsong-recognition-dataset>=0.3.2", + "evfuncs>=0.3.5", + "importlib-resources>=5.7.1", + "numpy>=1.18.1", + "pandas>=1.3.5", + "pandera>=0.9.0", + "scipy>=1.4.1", +] +files = [ + {file = "crowsetta-4.0.0.post2-py3-none-any.whl", hash = "sha256:255cfb972ab1749f81424fcdffc314e52f17e887a2f616ef0b0174c7a0bf1f54"}, + {file = "crowsetta-4.0.0.post2.tar.gz", hash = "sha256:c2795ff339d662bc8d07873199f902210ff980b29a4db04672433867cf3e1df6"}, ] [[package]] @@ -474,47 +518,52 @@ files = [ [[package]] name = "cython" -version = "3.0.7" +version = "3.0.8" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" summary = "The Cython compiler for writing C extensions in the Python language." files = [ - {file = "Cython-3.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3c0e19bb41de6be9d8afc85795159ca16296be81a586cd9588be0400d44a855"}, - {file = "Cython-3.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e8bf00ec1dd1d92e9ae74d2e6891f087a939e1dfb40c9c7fa5d8d6a26c94f5a"}, - {file = "Cython-3.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd6ae43ef2e596c9a88dbf2a8895be2e32cc2f5bc3c8ba2e7753b69068fc0b2d"}, - {file = "Cython-3.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f674be92673e87dd8ee7cfe553d5960ec4effc5ab15063b9a5e265a51585a31a"}, - {file = "Cython-3.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:861cf254bf5836d47c2aee86aa75dd93d3de00ccd1b077c3c7a2bb22cba358e7"}, - {file = "Cython-3.0.7-cp310-cp310-win32.whl", hash = "sha256:f6d8ff62ad55dc0393686438eac4b457a916e4d1118a0b550746bb52b4c756cc"}, - {file = "Cython-3.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:e13abb14843397b76d0472c7d33cd260d5f262ab05cc27ed423317e645e29643"}, - {file = "Cython-3.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c636c9ab92c7838231a1ba769e519d953af8294612f3f772a54d3a5250ff23f"}, - {file = "Cython-3.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22d2a684122dfb531853d57c8c85c1d5d44be709e12466dca99fa6aee7d8054f"}, - {file = "Cython-3.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1bdf8a107fdf9e174991aa87a0be7504f60de1ec6bfb1ccfb30e33acac818a0"}, - {file = "Cython-3.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3a83e04fde663b84905f3a20213a4333d13a07b79434300704b70dc552761f8b"}, - {file = "Cython-3.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e34b4b08d795ccca920fa26b099558f4f1e4e3f794e4ba8d3433c5bc2454d50a"}, - {file = "Cython-3.0.7-cp311-cp311-win32.whl", hash = "sha256:133057ac45b6fa7fe5d7baada9d3545d09339432f75c0545f556e8c6fecc2932"}, - {file = "Cython-3.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:b65abca78aa5ebc8675c8480b9a53006f6efea9910ad099cf32c9fb5617ef251"}, - {file = "Cython-3.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ceac5315fe899c229e874328742154e331fa41337bb03f6f5264636c351c9e"}, - {file = "Cython-3.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea936cf5931297ba07bce121388c4c6266c1b63a9f4d648ae16c92ff090204b"}, - {file = "Cython-3.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fcd9a18ee3ac7f460e0841954feb495102ffbdbec0e6c78562f3495cda000dd"}, - {file = "Cython-3.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7c8d579d13cb81abe704c8b0908d122b81d6e2623265a19c4a6a7377f440debb"}, - {file = "Cython-3.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ef5bb0268bfe5992da3ef9292463a5a895ed8700b134ed2c00008d5471b3ba6e"}, - {file = "Cython-3.0.7-cp312-cp312-win32.whl", hash = "sha256:55f93d3822bc196b37a8bdfa4ec6a35232a399e97f2baa714bd5ed8ea9b0ce68"}, - {file = "Cython-3.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:f3845c4506e0d207c5e268fb02813928f3a1e135de954a379f165ef0d581da47"}, - {file = "Cython-3.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c67105f2c6ccf5b3adbcfaecf3c5c9fa8940f9f97955c9ad7d2542151d97d93"}, - {file = "Cython-3.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a1859af761977530df2cd5c36e31d54e8d6708ad2c4656e7125c482364dc216"}, - {file = "Cython-3.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01b94304aab87496e81d1f546e71abf57b430b39be4269df1cd7da9928d70b5b"}, - {file = "Cython-3.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:931aade65f77cf59f2a702ac1f549a4836ce221107c740502cbad18d6d8e9511"}, - {file = "Cython-3.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:812b193c26553f1f375d4f1c50f805c227b24ed2d595bc9cdaf78c992ecc64a4"}, - {file = "Cython-3.0.7-cp38-cp38-win32.whl", hash = "sha256:b227643d8a40b68554dc7d37fcd03fc97b4fb0bd2614aeb5f2e07ab244642d36"}, - {file = "Cython-3.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:0d8a98c7d86ac4d05b251c39faf49423780381aab55fbf2e147f6e006a34a58a"}, - {file = "Cython-3.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816f5285d596062c7ef22790de7d75354b58d4417a9fc64cba914aeeb900db0b"}, - {file = "Cython-3.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d0dae6dccd349b8ccf197c10ef2d05c711ca36a649c7eddbab1de2c90b63a1"}, - {file = "Cython-3.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13211b67b29f6ed8e87c137496c73d93aff0330d97940b4fbed72eae37a4a2a0"}, - {file = "Cython-3.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b1853bc34ced5ff6473e881fcf6de29da83262552c8f268a0df53b49c2b89e2c"}, - {file = "Cython-3.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:51e8164b1270625ff101e95c3c1c234421520c07a0a3a20ded9e9431d98afce7"}, - {file = "Cython-3.0.7-cp39-cp39-win32.whl", hash = "sha256:45319d2471f4dbf19893ca53785a421107266e18b8cccd2054fce1e3f72a85f1"}, - {file = "Cython-3.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:612d83fd1eb5aaa5401a755c1f1aafacd9dab404cd350b90d5f404c98b33e4b3"}, - {file = "Cython-3.0.7-py2.py3-none-any.whl", hash = "sha256:936ec37b261b226d7404eff23a9aad284098338150d42a53d6a9af12b18d3892"}, - {file = "Cython-3.0.7.tar.gz", hash = "sha256:fb299acf3a578573c190c858d49e0cf9d75f4bc49c3f24c5a63804997ef09213"}, + {file = "Cython-3.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a846e0a38e2b24e9a5c5dc74b0e54c6e29420d88d1dafabc99e0fc0f3e338636"}, + {file = "Cython-3.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45523fdc2b78d79b32834cc1cc12dc2ca8967af87e22a3ee1bff20e77c7f5520"}, + {file = "Cython-3.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa0b7f3f841fe087410cab66778e2d3fb20ae2d2078a2be3dffe66c6574be39"}, + {file = "Cython-3.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87294e33e40c289c77a135f491cd721bd089f193f956f7b8ed5aa2d0b8c558f"}, + {file = "Cython-3.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a1df7a129344b1215c20096d33c00193437df1a8fcca25b71f17c23b1a44f782"}, + {file = "Cython-3.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:13c2a5e57a0358da467d97667297bf820b62a1a87ae47c5f87938b9bb593acbd"}, + {file = "Cython-3.0.8-cp310-cp310-win32.whl", hash = "sha256:96b028f044f5880e3cb18ecdcfc6c8d3ce9d0af28418d5ab464509f26d8adf12"}, + {file = "Cython-3.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:8140597a8b5cc4f119a1190f5a2228a84f5ca6d8d9ec386cfce24663f48b2539"}, + {file = "Cython-3.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aae26f9663e50caf9657148403d9874eea41770ecdd6caf381d177c2b1bb82ba"}, + {file = "Cython-3.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:547eb3cdb2f8c6f48e6865d5a741d9dd051c25b3ce076fbca571727977b28ac3"}, + {file = "Cython-3.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a567d4b9ba70b26db89d75b243529de9e649a2f56384287533cf91512705bee"}, + {file = "Cython-3.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51d1426263b0e82fb22bda8ea60dc77a428581cc19e97741011b938445d383f1"}, + {file = "Cython-3.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c26daaeccda072459b48d211415fd1e5507c06bcd976fa0d5b8b9f1063467d7b"}, + {file = "Cython-3.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:289ce7838208211cd166e975865fd73b0649bf118170b6cebaedfbdaf4a37795"}, + {file = "Cython-3.0.8-cp311-cp311-win32.whl", hash = "sha256:c8aa05f5e17f8042a3be052c24f2edc013fb8af874b0bf76907d16c51b4e7871"}, + {file = "Cython-3.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:000dc9e135d0eec6ecb2b40a5b02d0868a2f8d2e027a41b0fe16a908a9e6de02"}, + {file = "Cython-3.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:90d3fe31db55685d8cb97d43b0ec39ef614fcf660f83c77ed06aa670cb0e164f"}, + {file = "Cython-3.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e24791ddae2324e88e3c902a765595c738f19ae34ee66bfb1a6dac54b1833419"}, + {file = "Cython-3.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f020fa1c0552052e0660790b8153b79e3fc9a15dbd8f1d0b841fe5d204a6ae6"}, + {file = "Cython-3.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18bfa387d7a7f77d7b2526af69a65dbd0b731b8d941aaff5becff8e21f6d7717"}, + {file = "Cython-3.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fe81b339cffd87c0069c6049b4d33e28bdd1874625ee515785bf42c9fdff3658"}, + {file = "Cython-3.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80fd94c076e1e1b1ee40a309be03080b75f413e8997cddcf401a118879863388"}, + {file = "Cython-3.0.8-cp312-cp312-win32.whl", hash = "sha256:85077915a93e359a9b920280d214dc0cf8a62773e1f3d7d30fab8ea4daed670c"}, + {file = "Cython-3.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:0cb2dcc565c7851f75d496f724a384a790fab12d1b82461b663e66605bec429a"}, + {file = "Cython-3.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2cde23c555470db3f149ede78b518e8274853745289c956a0e06ad8d982e4db9"}, + {file = "Cython-3.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7990ca127e1f1beedaf8fc8bf66541d066ef4723ad7d8d47a7cbf842e0f47580"}, + {file = "Cython-3.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b983c8e6803f016146c26854d9150ddad5662960c804ea7f0c752c9266752f0"}, + {file = "Cython-3.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a973268d7ca1a2bdf78575e459a94a78e1a0a9bb62a7db0c50041949a73b02ff"}, + {file = "Cython-3.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:61a237bc9dd23c7faef0fcfce88c11c65d0c9bb73c74ccfa408b3a012073c20e"}, + {file = "Cython-3.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3a3d67f079598af49e90ff9655bf85bd358f093d727eb21ca2708f467c489cae"}, + {file = "Cython-3.0.8-cp38-cp38-win32.whl", hash = "sha256:17a642bb01a693e34c914106566f59844b4461665066613913463a719e0dd15d"}, + {file = "Cython-3.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:2cdfc32252f3b6dc7c94032ab744dcedb45286733443c294d8f909a4854e7f83"}, + {file = "Cython-3.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa97893d99385386925d00074654aeae3a98867f298d1e12ceaf38a9054a9bae"}, + {file = "Cython-3.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05c0bf9d085c031df8f583f0d506aa3be1692023de18c45d0aaf78685bbb944"}, + {file = "Cython-3.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de892422582f5758bd8de187e98ac829330ec1007bc42c661f687792999988a7"}, + {file = "Cython-3.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:314f2355a1f1d06e3c431eaad4708cf10037b5e91e4b231d89c913989d0bdafd"}, + {file = "Cython-3.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:78825a3774211e7d5089730f00cdf7f473042acc9ceb8b9eeebe13ed3a5541de"}, + {file = "Cython-3.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:df8093deabc55f37028190cf5e575c26aad23fc673f34b85d5f45076bc37ce39"}, + {file = "Cython-3.0.8-cp39-cp39-win32.whl", hash = "sha256:1aca1b97e0095b3a9a6c33eada3f661a4ed0d499067d121239b193e5ba3bb4f0"}, + {file = "Cython-3.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:16873d78be63bd38ffb759da7ab82814b36f56c769ee02b1d5859560e4c3ac3c"}, + {file = "Cython-3.0.8-py2.py3-none-any.whl", hash = "sha256:171b27051253d3f9108e9759e504ba59ff06e7f7ba944457f94deaf9c21bf0b6"}, + {file = "Cython-3.0.8.tar.gz", hash = "sha256:8333423d8fd5765e7cceea3a9985dd1e0a5dfeb2734629e1a2ed2d6233d39de6"}, ] [[package]] @@ -558,6 +607,20 @@ files = [ {file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"}, ] +[[package]] +name = "evfuncs" +version = "0.3.5.post1" +requires_python = ">=3.8" +summary = "Functions for working with files created by the EvTAF program and the evsonganaly GUI" +dependencies = [ + "numpy>=1.18.1", + "scipy>=1.2.0", +] +files = [ + {file = "evfuncs-0.3.5.post1-py3-none-any.whl", hash = "sha256:2ef3c996a8ad468a34c428c8c376579fb95896cb459495ffeaaccc71ef4c580b"}, + {file = "evfuncs-0.3.5.post1.tar.gz", hash = "sha256:3b73bc49f9e22c0e78eb00be32df0d03d88c72f90373657b36cb03c02a6bb75a"}, +] + [[package]] name = "exceptiongroup" version = "1.2.0" @@ -669,7 +732,7 @@ files = [ [[package]] name = "hypothesis" -version = "6.92.2" +version = "6.97.4" requires_python = ">=3.8" summary = "A library for property-based testing" dependencies = [ @@ -678,8 +741,8 @@ dependencies = [ "sortedcontainers<3.0.0,>=2.1.0", ] files = [ - {file = "hypothesis-6.92.2-py3-none-any.whl", hash = "sha256:d335044492acb03fa1fdb4edacb81cca2e578049fc7306345bc0e8947fef15a9"}, - {file = "hypothesis-6.92.2.tar.gz", hash = "sha256:841f89a486c43bdab55698de8929bd2635639ec20bf6ce98ccd75622d7ee6d41"}, + {file = "hypothesis-6.97.4-py3-none-any.whl", hash = "sha256:9069fe3fb18d9b7dd218bd69ab50bbc66426819dfac7cc7168ba85034d98a4df"}, + {file = "hypothesis-6.97.4.tar.gz", hash = "sha256:28ff724fa81ccc55f64f0f1eb06e4a75db6a195fe0857e9b3184cf4ff613a103"}, ] [[package]] @@ -1064,7 +1127,7 @@ files = [ [[package]] name = "mkdocs-gallery" -version = "0.9.0" +version = "0.10.0" summary = "a `mkdocs` plugin to generate example galleries from python scripts, similar to `sphinx-gallery`." dependencies = [ "mkdocs-material", @@ -1073,13 +1136,13 @@ dependencies = [ "tqdm", ] files = [ - {file = "mkdocs-gallery-0.9.0.tar.gz", hash = "sha256:f46ed505d7b710f554eade31fbb53074294cef8f9ad8e8aa39ee5875bef81be0"}, - {file = "mkdocs_gallery-0.9.0-py2.py3-none-any.whl", hash = "sha256:6ba4a3655422b1367895f67039faf413735871596d35d5ab73f206f1a809e12e"}, + {file = "mkdocs-gallery-0.10.0.tar.gz", hash = "sha256:7f5d79944794b585f1e0d9c3a1ad59be436bfa8bb375b6811d00af1b79731a1e"}, + {file = "mkdocs_gallery-0.10.0-py2.py3-none-any.whl", hash = "sha256:66523489d444da2b99b05b924473d7a592fb1fcc78dfa235f4ebdc554c7d419d"}, ] [[package]] name = "mkdocs-material" -version = "9.5.3" +version = "9.5.6" requires_python = ">=3.8" summary = "Documentation that simply works" dependencies = [ @@ -1096,8 +1159,8 @@ dependencies = [ "requests~=2.26", ] files = [ - {file = "mkdocs_material-9.5.3-py3-none-any.whl", hash = "sha256:76c93a8525cceb0b395b9cedab3428bf518cf6439adef2b940f1c1574b775d89"}, - {file = "mkdocs_material-9.5.3.tar.gz", hash = "sha256:5899219f422f0a6de784232d9d40374416302ffae3c160cacc72969fcc1ee372"}, + {file = "mkdocs_material-9.5.6-py3-none-any.whl", hash = "sha256:e115b90fccf5cd7f5d15b0c2f8e6246b21041628b8f590630e7fca66ed7fcf6c"}, + {file = "mkdocs_material-9.5.6.tar.gz", hash = "sha256:5b24df36d8ac6cecd611241ce6f6423ccde3e1ad89f8360c3f76d5565fc2d82a"}, ] [[package]] @@ -1161,6 +1224,16 @@ files = [ {file = "mkdocstrings-0.24.0.tar.gz", hash = "sha256:222b1165be41257b494a9d29b14135d2b7ca43f38161d5b10caae03b87bd4f7e"}, ] +[[package]] +name = "multimethod" +version = "1.10" +requires_python = ">=3.8" +summary = "Multiple argument dispatching." +files = [ + {file = "multimethod-1.10-py3-none-any.whl", hash = "sha256:afd84da9c3d0445c84f827e4d63ad42d17c6d29b122427c6dee9032ac2d2a0d4"}, + {file = "multimethod-1.10.tar.gz", hash = "sha256:daa45af3fe257f73abb69673fd54ddeaf31df0eb7363ad6e1251b7c9b192d8c5"}, +] + [[package]] name = "mypy" version = "1.8.0" @@ -1306,6 +1379,26 @@ files = [ {file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"}, ] +[[package]] +name = "pandera" +version = "0.18.0" +requires_python = ">=3.7" +summary = "A light-weight and flexible data validation and testing tool for statistical data objects." +dependencies = [ + "multimethod", + "numpy>=1.19.0", + "packaging>=20.0", + "pandas>=1.2.0", + "pydantic", + "typeguard>=3.0.2", + "typing-inspect>=0.6.0", + "wrapt", +] +files = [ + {file = "pandera-0.18.0-py3-none-any.whl", hash = "sha256:fe2da835a16df5a7e49fbfb828f1eeaea9d6f4534f124630957e64fef53e7e73"}, + {file = "pandera-0.18.0.tar.gz", hash = "sha256:97ab33d884362c0bb99668a12be2855d15c1a71f4934c588a999947b47764bc1"}, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -1439,121 +1532,107 @@ files = [ [[package]] name = "pydantic" -version = "2.5.3" -requires_python = ">=3.7" +version = "2.6.0" +requires_python = ">=3.8" summary = "Data validation using Python type hints" dependencies = [ "annotated-types>=0.4.0", - "pydantic-core==2.14.6", + "pydantic-core==2.16.1", "typing-extensions>=4.6.1", ] files = [ - {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, - {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, + {file = "pydantic-2.6.0-py3-none-any.whl", hash = "sha256:1440966574e1b5b99cf75a13bec7b20e3512e8a61b894ae252f56275e2c465ae"}, + {file = "pydantic-2.6.0.tar.gz", hash = "sha256:ae887bd94eb404b09d86e4d12f93893bdca79d766e738528c6fa1c849f3c6bcf"}, ] [[package]] name = "pydantic-core" -version = "2.14.6" -requires_python = ">=3.7" +version = "2.16.1" +requires_python = ">=3.8" summary = "" dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, - {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, - {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, - {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, - {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, - {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, - {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, - {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, - {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, - {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, - {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, - {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, - {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, - {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, - {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, - {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, - {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, - {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, - {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, - {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, - {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, - {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, - {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, - {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, - {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, - {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, - {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, - {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, - {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, - {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, - {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, - {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, - {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:300616102fb71241ff477a2cbbc847321dbec49428434a2f17f37528721c4948"}, + {file = "pydantic_core-2.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5511f962dd1b9b553e9534c3b9c6a4b0c9ded3d8c2be96e61d56f933feef9e1f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98f0edee7ee9cc7f9221af2e1b95bd02810e1c7a6d115cfd82698803d385b28f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9795f56aa6b2296f05ac79d8a424e94056730c0b860a62b0fdcfe6340b658cc8"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c45f62e4107ebd05166717ac58f6feb44471ed450d07fecd90e5f69d9bf03c48"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:462d599299c5971f03c676e2b63aa80fec5ebc572d89ce766cd11ca8bcb56f3f"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ebaa4bf6386a3b22eec518da7d679c8363fb7fb70cf6972161e5542f470798"}, + {file = "pydantic_core-2.16.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:99f9a50b56713a598d33bc23a9912224fc5d7f9f292444e6664236ae471ddf17"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8ec364e280db4235389b5e1e6ee924723c693cbc98e9d28dc1767041ff9bc388"}, + {file = "pydantic_core-2.16.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:653a5dfd00f601a0ed6654a8b877b18d65ac32c9d9997456e0ab240807be6cf7"}, + {file = "pydantic_core-2.16.1-cp310-none-win32.whl", hash = "sha256:1661c668c1bb67b7cec96914329d9ab66755911d093bb9063c4c8914188af6d4"}, + {file = "pydantic_core-2.16.1-cp310-none-win_amd64.whl", hash = "sha256:561be4e3e952c2f9056fba5267b99be4ec2afadc27261505d4992c50b33c513c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:102569d371fadc40d8f8598a59379c37ec60164315884467052830b28cc4e9da"}, + {file = "pydantic_core-2.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:735dceec50fa907a3c314b84ed609dec54b76a814aa14eb90da31d1d36873a5e"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e83ebbf020be727d6e0991c1b192a5c2e7113eb66e3def0cd0c62f9f266247e4"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:30a8259569fbeec49cfac7fda3ec8123486ef1b729225222f0d41d5f840b476f"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:920c4897e55e2881db6a6da151198e5001552c3777cd42b8a4c2f72eedc2ee91"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5247a3d74355f8b1d780d0f3b32a23dd9f6d3ff43ef2037c6dcd249f35ecf4c"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5bea8012df5bb6dda1e67d0563ac50b7f64a5d5858348b5c8cb5043811c19d"}, + {file = "pydantic_core-2.16.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ed3025a8a7e5a59817b7494686d449ebfbe301f3e757b852c8d0d1961d6be864"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:06f0d5a1d9e1b7932477c172cc720b3b23c18762ed7a8efa8398298a59d177c7"}, + {file = "pydantic_core-2.16.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:150ba5c86f502c040b822777e2e519b5625b47813bd05f9273a8ed169c97d9ae"}, + {file = "pydantic_core-2.16.1-cp311-none-win32.whl", hash = "sha256:d6cbdf12ef967a6aa401cf5cdf47850559e59eedad10e781471c960583f25aa1"}, + {file = "pydantic_core-2.16.1-cp311-none-win_amd64.whl", hash = "sha256:afa01d25769af33a8dac0d905d5c7bb2d73c7c3d5161b2dd6f8b5b5eea6a3c4c"}, + {file = "pydantic_core-2.16.1-cp311-none-win_arm64.whl", hash = "sha256:1a2fe7b00a49b51047334d84aafd7e39f80b7675cad0083678c58983662da89b"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f478ec204772a5c8218e30eb813ca43e34005dff2eafa03931b3d8caef87d51"}, + {file = "pydantic_core-2.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1936ef138bed2165dd8573aa65e3095ef7c2b6247faccd0e15186aabdda7f66"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99d3a433ef5dc3021c9534a58a3686c88363c591974c16c54a01af7efd741f13"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd88f40f2294440d3f3c6308e50d96a0d3d0973d6f1a5732875d10f569acef49"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fac641bbfa43d5a1bed99d28aa1fded1984d31c670a95aac1bf1d36ac6ce137"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72bf9308a82b75039b8c8edd2be2924c352eda5da14a920551a8b65d5ee89253"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb4363e6c9fc87365c2bc777a1f585a22f2f56642501885ffc7942138499bf54"}, + {file = "pydantic_core-2.16.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:20f724a023042588d0f4396bbbcf4cffd0ddd0ad3ed4f0d8e6d4ac4264bae81e"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fb4370b15111905bf8b5ba2129b926af9470f014cb0493a67d23e9d7a48348e8"}, + {file = "pydantic_core-2.16.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23632132f1fd608034f1a56cc3e484be00854db845b3a4a508834be5a6435a6f"}, + {file = "pydantic_core-2.16.1-cp312-none-win32.whl", hash = "sha256:b9f3e0bffad6e238f7acc20c393c1ed8fab4371e3b3bc311020dfa6020d99212"}, + {file = "pydantic_core-2.16.1-cp312-none-win_amd64.whl", hash = "sha256:a0b4cfe408cd84c53bab7d83e4209458de676a6ec5e9c623ae914ce1cb79b96f"}, + {file = "pydantic_core-2.16.1-cp312-none-win_arm64.whl", hash = "sha256:d195add190abccefc70ad0f9a0141ad7da53e16183048380e688b466702195dd"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:502c062a18d84452858f8aea1e520e12a4d5228fc3621ea5061409d666ea1706"}, + {file = "pydantic_core-2.16.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d8c032ccee90b37b44e05948b449a2d6baed7e614df3d3f47fe432c952c21b60"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:920f4633bee43d7a2818e1a1a788906df5a17b7ab6fe411220ed92b42940f818"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f5d37ff01edcbace53a402e80793640c25798fb7208f105d87a25e6fcc9ea06"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:399166f24c33a0c5759ecc4801f040dbc87d412c1a6d6292b2349b4c505effc9"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac89ccc39cd1d556cc72d6752f252dc869dde41c7c936e86beac5eb555041b66"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73802194f10c394c2bedce7a135ba1d8ba6cff23adf4217612bfc5cf060de34c"}, + {file = "pydantic_core-2.16.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fa00fa24ffd8c31fac081bf7be7eb495be6d248db127f8776575a746fa55c95"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:601d3e42452cd4f2891c13fa8c70366d71851c1593ed42f57bf37f40f7dca3c8"}, + {file = "pydantic_core-2.16.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07982b82d121ed3fc1c51faf6e8f57ff09b1325d2efccaa257dd8c0dd937acca"}, + {file = "pydantic_core-2.16.1-cp38-none-win32.whl", hash = "sha256:d0bf6f93a55d3fa7a079d811b29100b019784e2ee6bc06b0bb839538272a5610"}, + {file = "pydantic_core-2.16.1-cp38-none-win_amd64.whl", hash = "sha256:fbec2af0ebafa57eb82c18c304b37c86a8abddf7022955d1742b3d5471a6339e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a497be217818c318d93f07e14502ef93d44e6a20c72b04c530611e45e54c2196"}, + {file = "pydantic_core-2.16.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:694a5e9f1f2c124a17ff2d0be613fd53ba0c26de588eb4bdab8bca855e550d95"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d4dfc66abea3ec6d9f83e837a8f8a7d9d3a76d25c9911735c76d6745950e62c"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8655f55fe68c4685673265a650ef71beb2d31871c049c8b80262026f23605ee3"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21e3298486c4ea4e4d5cc6fb69e06fb02a4e22089304308817035ac006a7f506"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71b4a48a7427f14679f0015b13c712863d28bb1ab700bd11776a5368135c7d60"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dca874e35bb60ce4f9f6665bfbfad050dd7573596608aeb9e098621ac331dc"}, + {file = "pydantic_core-2.16.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa496cd45cda0165d597e9d6f01e36c33c9508f75cf03c0a650018c5048f578e"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5317c04349472e683803da262c781c42c5628a9be73f4750ac7d13040efb5d2d"}, + {file = "pydantic_core-2.16.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42c29d54ed4501a30cd71015bf982fa95e4a60117b44e1a200290ce687d3e640"}, + {file = "pydantic_core-2.16.1-cp39-none-win32.whl", hash = "sha256:ba07646f35e4e49376c9831130039d1b478fbfa1215ae62ad62d2ee63cf9c18f"}, + {file = "pydantic_core-2.16.1-cp39-none-win_amd64.whl", hash = "sha256:2133b0e412a47868a358713287ff9f9a328879da547dc88be67481cdac529118"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d25ef0c33f22649b7a088035fd65ac1ce6464fa2876578df1adad9472f918a76"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:99c095457eea8550c9fa9a7a992e842aeae1429dab6b6b378710f62bfb70b394"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b49c604ace7a7aa8af31196abbf8f2193be605db6739ed905ecaf62af31ccae0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c56da23034fe66221f2208c813d8aa509eea34d97328ce2add56e219c3a9f41c"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cebf8d56fee3b08ad40d332a807ecccd4153d3f1ba8231e111d9759f02edfd05"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1ae8048cba95f382dba56766525abca438328455e35c283bb202964f41a780b0"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:780daad9e35b18d10d7219d24bfb30148ca2afc309928e1d4d53de86822593dc"}, + {file = "pydantic_core-2.16.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c94b5537bf6ce66e4d7830c6993152940a188600f6ae044435287753044a8fe2"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:adf28099d061a25fbcc6531febb7a091e027605385de9fe14dd6a97319d614cf"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:644904600c15816a1f9a1bafa6aab0d21db2788abcdf4e2a77951280473f33e1"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87bce04f09f0552b66fca0c4e10da78d17cb0e71c205864bab4e9595122cb9d9"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877045a7969ace04d59516d5d6a7dee13106822f99a5d8df5e6822941f7bedc8"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9c46e556ee266ed3fb7b7a882b53df3c76b45e872fdab8d9cf49ae5e91147fd7"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4eebbd049008eb800f519578e944b8dc8e0f7d59a5abb5924cc2d4ed3a1834ff"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c0be58529d43d38ae849a91932391eb93275a06b93b79a8ab828b012e916a206"}, + {file = "pydantic_core-2.16.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b1fc07896fc1851558f532dffc8987e526b682ec73140886c831d773cef44b76"}, + {file = "pydantic_core-2.16.1.tar.gz", hash = "sha256:daff04257b49ab7f4b3f73f98283d3dbb1a65bf3500d55c7beac3c66c310fe34"}, ] [[package]] @@ -1874,27 +1953,27 @@ files = [ [[package]] name = "ruff" -version = "0.1.11" +version = "0.2.0" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a7f772696b4cdc0a3b2e527fc3c7ccc41cdcb98f5c80fdd4f2b8c50eb1458196"}, - {file = "ruff-0.1.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934832f6ed9b34a7d5feea58972635c2039c7a3b434fe5ba2ce015064cb6e955"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea0d3e950e394c4b332bcdd112aa566010a9f9c95814844a7468325290aabfd9"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bd4025b9c5b429a48280785a2b71d479798a69f5c2919e7d274c5f4b32c3607"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1ad00662305dcb1e987f5ec214d31f7d6a062cae3e74c1cbccef15afd96611d"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4b077ce83f47dd6bea1991af08b140e8b8339f0ba8cb9b7a484c30ebab18a23f"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4a88efecec23c37b11076fe676e15c6cdb1271a38f2b415e381e87fe4517f18"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b25093dad3b055667730a9b491129c42d45e11cdb7043b702e97125bcec48a1"}, - {file = "ruff-0.1.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231d8fb11b2cc7c0366a326a66dafc6ad449d7fcdbc268497ee47e1334f66f77"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:09c415716884950080921dd6237767e52e227e397e2008e2bed410117679975b"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f58948c6d212a6b8d41cd59e349751018797ce1727f961c2fa755ad6208ba45"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:190a566c8f766c37074d99640cd9ca3da11d8deae2deae7c9505e68a4a30f740"}, - {file = "ruff-0.1.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6464289bd67b2344d2a5d9158d5eb81025258f169e69a46b741b396ffb0cda95"}, - {file = "ruff-0.1.11-py3-none-win32.whl", hash = "sha256:9b8f397902f92bc2e70fb6bebfa2139008dc72ae5177e66c383fa5426cb0bf2c"}, - {file = "ruff-0.1.11-py3-none-win_amd64.whl", hash = "sha256:eb85ee287b11f901037a6683b2374bb0ec82928c5cbc984f575d0437979c521a"}, - {file = "ruff-0.1.11-py3-none-win_arm64.whl", hash = "sha256:97ce4d752f964ba559c7023a86e5f8e97f026d511e48013987623915431c7ea9"}, - {file = "ruff-0.1.11.tar.gz", hash = "sha256:f9d4d88cb6eeb4dfe20f9f0519bd2eaba8119bde87c3d5065c541dbae2b5a2cb"}, + {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:638ea3294f800d18bae84a492cb5a245c8d29c90d19a91d8e338937a4c27fca0"}, + {file = "ruff-0.2.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff35433fcf4dff6d610738712152df6b7d92351a1bde8e00bd405b08b3d5759"}, + {file = "ruff-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf9faafbdcf4f53917019f2c230766da437d4fd5caecd12ddb68bb6a17d74399"}, + {file = "ruff-0.2.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8153a3e4128ed770871c47545f1ae7b055023e0c222ff72a759f5a341ee06483"}, + {file = "ruff-0.2.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a75a98ae989a27090e9c51f763990ad5bbc92d20626d54e9701c7fe597f399"}, + {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:87057dd2fdde297130ff99553be8549ca38a2965871462a97394c22ed2dfc19d"}, + {file = "ruff-0.2.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d232f99d3ab00094ebaf88e0fb7a8ccacaa54cc7fa3b8993d9627a11e6aed7a"}, + {file = "ruff-0.2.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3c641f95f435fc6754b05591774a17df41648f0daf3de0d75ad3d9f099ab92"}, + {file = "ruff-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3826fb34c144ef1e171b323ed6ae9146ab76d109960addca730756dc19dc7b22"}, + {file = "ruff-0.2.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:eceab7d85d09321b4de18b62d38710cf296cb49e98979960a59c6b9307c18cfe"}, + {file = "ruff-0.2.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:30ad74687e1f4a9ff8e513b20b82ccadb6bd796fe5697f1e417189c5cde6be3e"}, + {file = "ruff-0.2.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7e3818698f8460bd0f8d4322bbe99db8327e9bc2c93c789d3159f5b335f47da"}, + {file = "ruff-0.2.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:edf23041242c48b0d8295214783ef543847ef29e8226d9f69bf96592dba82a83"}, + {file = "ruff-0.2.0-py3-none-win32.whl", hash = "sha256:e155147199c2714ff52385b760fe242bb99ea64b240a9ffbd6a5918eb1268843"}, + {file = "ruff-0.2.0-py3-none-win_amd64.whl", hash = "sha256:ba918e01cdd21e81b07555564f40d307b0caafa9a7a65742e98ff244f5035c59"}, + {file = "ruff-0.2.0-py3-none-win_arm64.whl", hash = "sha256:3fbaff1ba9564a2c5943f8f38bc221f04bac687cc7485e45237579fee7ccda79"}, + {file = "ruff-0.2.0.tar.gz", hash = "sha256:63856b91837606c673537d2889989733d7dffde553828d3b0f0bacfa6def54be"}, ] [[package]] @@ -2075,24 +2154,24 @@ files = [ [[package]] name = "tox" -version = "4.11.4" +version = "4.12.1" requires_python = ">=3.8" summary = "tox is a generic virtualenv management and test command line tool" dependencies = [ - "cachetools>=5.3.1", + "cachetools>=5.3.2", "chardet>=5.2", "colorama>=0.4.6", - "filelock>=3.12.3", - "packaging>=23.1", - "platformdirs>=3.10", + "filelock>=3.13.1", + "packaging>=23.2", + "platformdirs>=4.1", "pluggy>=1.3", "pyproject-api>=1.6.1", "tomli>=2.0.1; python_version < \"3.11\"", - "virtualenv>=20.24.3", + "virtualenv>=20.25", ] files = [ - {file = "tox-4.11.4-py3-none-any.whl", hash = "sha256:2adb83d68f27116812b69aa36676a8d6a52249cb0d173649de0e7d0c2e3e7229"}, - {file = "tox-4.11.4.tar.gz", hash = "sha256:73a7240778fabf305aeb05ab8ea26e575e042ab5a18d71d0ed13e343a51d6ce1"}, + {file = "tox-4.12.1-py3-none-any.whl", hash = "sha256:c07ea797880a44f3c4f200ad88ad92b446b83079d4ccef89585df64cc574375c"}, + {file = "tox-4.12.1.tar.gz", hash = "sha256:61aafbeff1bd8a5af84e54ef6e8402f53c6a6066d0782336171ddfbf5362122e"}, ] [[package]] @@ -2108,6 +2187,20 @@ files = [ {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, ] +[[package]] +name = "typeguard" +version = "4.1.5" +requires_python = ">=3.8" +summary = "Run-time type checker for Python" +dependencies = [ + "importlib-metadata>=3.6; python_version < \"3.10\"", + "typing-extensions>=4.7.0; python_version < \"3.12\"", +] +files = [ + {file = "typeguard-4.1.5-py3-none-any.whl", hash = "sha256:8923e55f8873caec136c892c3bed1f676eae7be57cdb94819281b3d3bc9c0953"}, + {file = "typeguard-4.1.5.tar.gz", hash = "sha256:ea0a113bbc111bcffc90789ebb215625c963411f7096a7e9062d4e4630c155fd"}, +] + [[package]] name = "typing-extensions" version = "4.9.0" @@ -2118,6 +2211,19 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "typing-inspect" +version = "0.9.0" +summary = "Runtime inspection utilities for typing module." +dependencies = [ + "mypy-extensions>=0.3.0", + "typing-extensions>=3.7.4", +] +files = [ + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, +] + [[package]] name = "tzdata" version = "2023.4" @@ -2196,6 +2302,66 @@ files = [ {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, ] +[[package]] +name = "wrapt" +version = "1.16.0" +requires_python = ">=3.6" +summary = "Module for decorators, wrappers and monkey patching." +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "xarray" version = "2023.1.0" diff --git a/pyproject.toml b/pyproject.toml index bf18789..f6c51d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,13 +28,14 @@ all = [ "soundevent[evaluation]", "soundevent[geometry]", ] - +crowsetta = [ + "crowsetta>=4.0.0.post2", +] [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" - [tool.pdm] [tool.pdm.dev-dependencies] dev = [ @@ -66,9 +67,20 @@ wrap-descriptions = 79 [tool.black] line-length = 79 +[tool.isort] +profile = "black" +line_length = 79 + [tool.pytest.ini_options] addopts = "-vv" +[tool.ruff] +line-length = 79 +target-version = "py38" + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + [tool.pyright] venvPath = "." venv = ".venv" @@ -80,9 +92,6 @@ convention = "numpy" match = "(?!test_).*\\.py" match_dir = "(?!tests).*" -[tool.isort] -profile = "black" - [tool.mypy] ignore_missing_imports = true show_error_codes = true diff --git a/src/soundevent/data/recordings.py b/src/soundevent/data/recordings.py index 237a831..088d23a 100644 --- a/src/soundevent/data/recordings.py +++ b/src/soundevent/data/recordings.py @@ -121,6 +121,27 @@ class Recording(BaseModel): notes : List[Note] A list of notes associated with the recording. Default is an empty list. + + Notes + ----- + When dealing with time-expanded recordings, adjustments are made to both + the duration and sample rate based on the time expansion factor. As a + result, the duration and sample rate stored in the Recording object may + deviate from the values derived from the file metadata. + + A time-expanded recording is one that has been either slowed down or sped + up, typically to facilitate the playback and analysis of audio data. In + these cases, although the audio was originally captured at a specific + sample rate, the resulting audio file exhibits a different sample rate due + to the time expansion process. The time expansion factor represents the + ratio of the original sample rate to the sample rate of the time-expanded + recording. + + Since the actual sample rate used to capture the audio differs from the + stored sample rate in the audio file, we have chosen to store the true + sample rate in the Recording object. This ensures that the Recording object + accurately reflects the sample rate at which the audio was originally + captured. """ uuid: UUID = Field(default_factory=uuid4, repr=False) diff --git a/src/soundevent/io/__init__.py b/src/soundevent/io/__init__.py index 97bb4e1..cce61a2 100644 --- a/src/soundevent/io/__init__.py +++ b/src/soundevent/io/__init__.py @@ -3,6 +3,8 @@ This module contains the classes and functions for reading and writing sound event data. """ + +import soundevent.io.crowsetta as crowsetta from soundevent.io.loader import load from soundevent.io.saver import save from soundevent.io.types import DataCollections @@ -11,4 +13,5 @@ "save", "load", "DataCollections", + "crowsetta", ] diff --git a/src/soundevent/io/crowsetta/__init__.py b/src/soundevent/io/crowsetta/__init__.py new file mode 100644 index 0000000..a933a8f --- /dev/null +++ b/src/soundevent/io/crowsetta/__init__.py @@ -0,0 +1,41 @@ +"""Crowsetta Module. + +This module provides a set of functions to facilitate the export and import of +data in the soundevent format to and from the Crowsetta format. +""" + +from soundevent.io.crowsetta.annotation import ( + annotation_from_clip_annotation, + annotation_to_clip_annotation, +) +from soundevent.io.crowsetta.bbox import ( + bbox_from_annotation, + bbox_to_annotation, +) +from soundevent.io.crowsetta.labels import ( + label_from_tag, + label_from_tags, + label_to_tags, +) +from soundevent.io.crowsetta.segment import ( + segment_from_annotation, + segment_to_annotation, +) +from soundevent.io.crowsetta.sequence import ( + sequence_from_annotations, + sequence_to_annotations, +) + +__all__ = [ + "annotation_from_clip_annotation", + "annotation_to_clip_annotation", + "bbox_from_annotation", + "bbox_to_annotation", + "label_from_tag", + "label_from_tags", + "label_to_tags", + "segment_from_annotation", + "segment_to_annotation", + "sequence_from_annotations", + "sequence_to_annotations", +] diff --git a/src/soundevent/io/crowsetta/annotation.py b/src/soundevent/io/crowsetta/annotation.py new file mode 100644 index 0000000..240c4c3 --- /dev/null +++ b/src/soundevent/io/crowsetta/annotation.py @@ -0,0 +1,237 @@ +import os +from pathlib import Path +from typing import List, Literal, Optional, Union + +import crowsetta + +from soundevent import data +from soundevent.io.crowsetta.bbox import ( + bbox_from_annotation, + bbox_to_annotation, +) +from soundevent.io.crowsetta.sequence import ( + sequence_from_annotations, + sequence_to_annotations, +) + +PathLike = Union[str, Path, os.PathLike] + + +__all__ = [ + "annotation_from_clip_annotation", + "annotation_to_clip_annotation", +] + + +def annotation_from_clip_annotation( + annot: data.ClipAnnotation, + annot_path: PathLike, + annotation_fmt: Union[Literal["bbox"], Literal["seq"]], + ignore_errors: bool = True, + cast_geometry: bool = True, + **kwargs, +) -> crowsetta.Annotation: + """Convert a ClipAnnotation to a Crowsetta annotation. + + This function transforms a `ClipAnnotation` object into a Crowsetta + annotation (`crowsetta.Annotation`). The choice of annotation format + (`bbox` for bounding boxes or `seq` for sequences) determines the type of + Crowsetta annotation created. Each sound event within the `ClipAnnotation` + is individually converted into either a sequence or a bounding boxes using + the corresponding conversion functions. + + Parameters + ---------- + annot : data.ClipAnnotation + The ClipAnnotation object to convert. + annot_path : PathLike + The path to the annotation file. + annotation_fmt : Union[Literal["bbox"], Literal["seq"]] + The desired Crowsetta annotation format: 'bbox' for bounding boxes or + 'seq' for sequences. + ignore_errors : bool, optional + If True, ignore errors during conversion and continue with the next + sound event, otherwise this function will raise an error if a sound + event cannot be converted, by default True. + cast_geometry : bool, optional + If True, cast non-matching geometries to the expected type, otherwise + raise an error if the geometry does not match the expected type, by + default True. + **kwargs + Additional keyword arguments passed to the corresponding conversion + functions. + + Returns + ------- + crowsetta.Annotation + A Crowsetta annotation representing the converted ClipAnnotation. + """ + if annotation_fmt == "bbox": + bboxes = [] + for annotation in annot.sound_events: + try: + bbox = bbox_from_annotation( + annotation, + cast_to_bbox=cast_geometry, + **kwargs, + ) + except ValueError as e: + if ignore_errors: + continue + raise e + bboxes.append(bbox) + return crowsetta.Annotation( + annot_path=annot_path, + notated_path=annot.clip.recording.path, + bboxes=bboxes, + ) + + if annotation_fmt != "seq": + raise ValueError( + "annotation_fmt must be either 'bbox' or 'seq', " + f"not {annotation_fmt}." + ) + + return crowsetta.Annotation( + annot_path=annot_path, + notated_path=annot.clip.recording.path, + seq=sequence_from_annotations( + annot.sound_events, + cast_to_segment=cast_geometry, + ignore_errors=ignore_errors, + **kwargs, + ), + ) + + +def annotation_to_clip_annotation( + annot: crowsetta.Annotation, + recording: Optional[data.Recording] = None, + tags: Optional[List[data.Tag]] = None, + notes: Optional[List[data.Note]] = None, + adjust_time_expansion: bool = True, + created_by: Optional[data.User] = None, + recording_kwargs: Optional[dict] = None, + **kwargs, +) -> data.ClipAnnotation: + """Convert a Crowsetta annotation to a ClipAnnotation. + + This function transforms a Crowsetta annotation (`crowsetta.Annotation`) + into a `ClipAnnotation` object. Depending on the annotation format, the + Crowsetta annotation is converted into a list of sound event annotations or + sequence annotations, or both, and included in the resulting + ClipAnnotation. + + Parameters + ---------- + annot : crowsetta.Annotation + The Crowsetta annotation to convert. + recording : Optional[data.Recording], optional + The original recording associated with the annotations, if not + provided, it is loaded based on the path of the notated recording, by + default None. + tags : Optional[List[data.Tag]], optional + Tags associated with the clip annotation, by default None. + notes : Optional[List[data.Note]], optional + Notes associated with the clip annotation, by default None. + adjust_time_expansion : bool, optional + If True, adjust the onset and offset times based on the recording's + time expansion factor, by default True. + created_by : Optional[data.User], optional + User information representing the creator of the converted clip + annotation, by default None. + recording_kwargs : Optional[dict], optional + Additional keyword arguments passed when loading the recording, + by default None. You can use this to set the recording + metadata such as `time_expansion`, `latitude`, `longitude`, etc. + **kwargs + Additional keyword arguments passed to the conversion functions. + + Returns + ------- + data.ClipAnnotation + A ClipAnnotation representing the converted Crowsetta annotation. + """ + + if tags is None: + tags = [] + + if notes is None: + notes = [] + + path = annot.notated_path + + if recording is None: + if path is None: + raise ValueError( + "A recording must be provided if the annotation does not " + "have an notated path." + ) + + if recording_kwargs is None: + recording_kwargs = {} + + path = Path(path) # type: ignore + recording = data.Recording.from_file(path, **recording_kwargs) + + if path is not None and path != recording.path: + raise ValueError( + "The path of the annotation does not match the path of the " + "recording." + ) + + sound_event_annotations = [] + sequence_annotations = [] + + crowsetta_bboxes: List[crowsetta.BBox] = getattr(annot, "bboxes", []) + for box in crowsetta_bboxes: + sound_event_annotations.append( + bbox_to_annotation( + box, + recording=recording, + adjust_time_expansion=adjust_time_expansion, + created_by=created_by, + **kwargs, + ) + ) + + crowsetta_sequences: Union[ + List[crowsetta.Sequence], crowsetta.Sequence + ] = getattr(annot, "seq", []) + + if not isinstance(crowsetta_sequences, list): + crowsetta_sequences = [crowsetta_sequences] + + for sequence in crowsetta_sequences: + time_interval_annotations = sequence_to_annotations( + sequence, + recording=recording, + adjust_time_expansion=adjust_time_expansion, + created_by=created_by, + **kwargs, + ) + sound_event_annotations.extend(time_interval_annotations) + + sequence_annotations.append( + data.SequenceAnnotation( + sequence=data.Sequence( + sound_events=[ + annotation.sound_event + for annotation in time_interval_annotations + ] + ), + created_by=created_by, + ) + ) + + return data.ClipAnnotation( + clip=data.Clip( + recording=recording, + start_time=0, + end_time=recording.duration, + ), + tags=tags, + notes=notes, + sound_events=sound_event_annotations, + sequences=sequence_annotations, + ) diff --git a/src/soundevent/io/crowsetta/bbox.py b/src/soundevent/io/crowsetta/bbox.py new file mode 100644 index 0000000..78e6a98 --- /dev/null +++ b/src/soundevent/io/crowsetta/bbox.py @@ -0,0 +1,181 @@ +from typing import List, Optional, Tuple + +from crowsetta import BBox + +from soundevent import data +from soundevent.geometry import compute_bounds +from soundevent.io.crowsetta.labels import label_from_tags, label_to_tags + +__all__ = [ + "bbox_from_annotation", + "bbox_to_annotation", +] + + +def convert_geometry_to_bbox( + geometry: data.Geometry, + cast_to_bbox: bool = True, + raise_on_time_geometries: bool = False, +) -> Tuple[float, float, float, float]: + if geometry.type != "BoundingBox" and not cast_to_bbox: + raise ValueError( + "Cannot convert to a crowsetta bbox " + "because the sound event geometry is not a BoundingBox." + ) + + if ( + geometry.type in ["TimeInterval", "TimeStamp"] + and raise_on_time_geometries + ): + raise ValueError( + "Cannot convert to a crowsetta bbox because " + "the sound event geometry is a TimeInterval or TimeStamp " + "and does not have frequency information." + ) + + return compute_bounds(geometry) + + +def bbox_from_annotation( + obj: data.SoundEventAnnotation, + cast_to_bbox: bool = True, + raise_on_time_geometries: bool = True, + **kwargs, +) -> BBox: + """Convert a soundevent annotation to a Crowsetta bounding box. + + This function transforms a SoundEventAnnotation object into a Crowsetta + bounding box (`BBox`). The SoundEvent's geometry is used to determine the + onset, offset, low frequency, and high frequency values of the bounding box, + and the associated tags are converted into a Crowsetta label using the + `convert_tags_to_label` function. + + Parameters + ---------- + obj : data.SoundEventAnnotation + The SoundEventAnnotation object to convert. + cast_to_bbox : bool, optional + If True, cast the geometry to a bounding box, otherwise, keep it as is, + by default True. + raise_on_time_geometries : bool, optional + If True, raise an exception if the geometry is a TimeInterval or + TimeStamp, otherwise, cast it to a bounding box with a lowest frequency + of 0 and a highest frequency of the recording's Nyquist frequency. By + default True. + **kwargs + Additional keyword arguments passed to the `convert_tags_to_label` + function. + + Returns + ------- + BBox + A Crowsetta bounding box representing the converted soundevent + annotation, with onset, offset, low frequency, high frequency, and + associated label. + """ + sound_event = obj.sound_event + geometry = sound_event.geometry + + if geometry is None: + raise ValueError( + "Cannot convert to a crowsetta bbox because the sound event " + "has no geometry." + ) + + start_time, low_freq, end_time, high_freq = convert_geometry_to_bbox( + geometry, + cast_to_bbox=cast_to_bbox, + raise_on_time_geometries=raise_on_time_geometries, + ) + + nyquist_freq = sound_event.recording.samplerate / 2 + high_freq = min(high_freq, nyquist_freq) + + label = label_from_tags( + obj.tags, + **kwargs, + ) + + return BBox( + onset=start_time, + offset=end_time, + low_freq=low_freq, + high_freq=high_freq, + label=label, + ) + + +def bbox_to_annotation( + bbox: BBox, + recording: data.Recording, + adjust_time_expansion: bool = True, + notes: Optional[List[data.Note]] = None, + created_by: Optional[data.User] = None, + **kwargs, +) -> data.SoundEventAnnotation: + """Convert a crowsetta bounding box annotation to a soundevent annotation. + + This function transforms a crowsetta bounding box annotation (`BBox`) into + a `SoundEventAnnotation` object. The onset, offset, low frequency, and high + frequency values of the bounding box are used to create a `BoundingBox` + geometry. The label associated with the bounding box is converted into a + list of soundevent tags using the `convert_label_to_tags` function. + + Parameters + ---------- + bbox : BBox + The bounding box annotation to convert. + recording : data.Recording + The original recording from which the bounding box annotation was + derived. + adjust_time_expansion : bool, optional + If True, adjust the onset, offset, and frequency values based on the + recording's time expansion factor, by default True. + notes : List[data.Note], optional + Additional notes associated with the converted soundevent annotation, + by default None. + created_by : data.User, optional + User information representing the creator of the converted soundevent + annotation, by default None. + **kwargs + Additional keyword arguments passed to the `convert_label_to_tags` + function. + + Returns + ------- + data.SoundEventAnnotation + A SoundEventAnnotation object representing the converted bounding box + annotation, containing a SoundEvent with the bounding box geometry, + associated tags, notes, and creator information. + """ + if notes is None: + notes = [] + + start_time = bbox.onset + end_time = bbox.offset + low_freq = bbox.low_freq + high_freq = bbox.high_freq + + if adjust_time_expansion and recording.time_expansion != 1: + start_time = start_time / recording.time_expansion + end_time = end_time / recording.time_expansion + low_freq = low_freq * recording.time_expansion + high_freq = high_freq * recording.time_expansion + + geometry = data.BoundingBox( + coordinates=[start_time, low_freq, end_time, high_freq] + ) + + tags = label_to_tags(bbox.label, **kwargs) + + sound_event = data.SoundEvent( + recording=recording, + geometry=geometry, + ) + + return data.SoundEventAnnotation( + sound_event=sound_event, + tags=tags, + notes=notes, + created_by=created_by, + ) diff --git a/src/soundevent/io/crowsetta/labels.py b/src/soundevent/io/crowsetta/labels.py new file mode 100644 index 0000000..456a630 --- /dev/null +++ b/src/soundevent/io/crowsetta/labels.py @@ -0,0 +1,245 @@ +"""Crowsetta label conversion functions. + +This module provides functions for converting between **Crowsetta** labels and +`soundevent` tags. Crowsetta labels are represented as strings, while +soundevent tags are instances of a custom class +[`data.Tag`][soundevent.data.Tag], which contains a key-value pair rather than +a single string. The conversion functions provided in this module facilitate +the conversion of labels to tags and vice versa, and they allow users to +customize the conversion process using various options. +""" + +from typing import Callable, List, Optional, Sequence, Union + +from soundevent import data + +__all__ = [ + "label_to_tags", + "label_from_tags", +] + + +EMPTY_LABEL = "__empty__" + + +LabelToTagFn = Callable[[str], Union[List[data.Tag], data.Tag]] +LabelToTagMap = dict[str, Union[List[data.Tag], data.Tag]] + + +def label_to_tags( + label: str, + tag_fn: Optional[LabelToTagFn] = None, + tag_mapping: Optional[LabelToTagMap] = None, + key_mapping: Optional[dict[str, str]] = None, + key: Optional[str] = None, + fallback: str = "crowsetta", + empty_labels: Sequence[str] = (EMPTY_LABEL,), +) -> List[data.Tag]: + """Convert a `crowsetta` label to a list of `soundevent` tags. + + This function facilitates the conversion of a **Crowsetta** label to a list + of `soundevent` tags. Users can customize the conversion process using the + following options: + + 1. If the label matches any of the `empty_labels`, the function returns + an empty list of tags. + 2. If a mapping function (`tag_fn` argument) is provided, it will be + used to directly convert the label to a list of tags. If the function + returns a single tag instead of a list, it is automatically wrapped in + a list. + 3. If a mapping dictionary (`tag_mapping` argument) is provided, the + function will attempt to look up the list of tags for the label in the + mapping. If found, it returns the list of tags; otherwise, it proceeds + to the next option. + 4. If a mapping dictionary (`key_mapping` argument) is provided, the + function will try to look up the key for the label in the mapping. + If found, it uses the key; otherwise, it proceeds to the next option. + 5. If the `key` argument is provided, it will be used as the key for + the tag. If `key` is not provided, the `fallback` argument will be used + as the key. + + Parameters + ---------- + label + The Crowsetta label to convert to a list of tags. + tag_fn + A function to convert labels to a list of tags. If a single tag is + returned, it is automatically wrapped in a list. + tag_mapping + A dictionary mapping labels to lists of tags or a single tag. + key_mapping + A dictionary mapping labels to keys. + key + The key to use for the tag. If not provided, the `fallback` argument + will be used. + fallback + The key to use if no other key is provided, by default "crowsetta". + empty_labels + A sequence of labels to be considered as empty, resulting in an empty + list of tags. + + Returns + ------- + List[data.Tag] + The list of soundevent tags corresponding to the Crowsetta label. + """ + if label in empty_labels: + return [] + + if tag_fn is not None: + try: + tags = tag_fn(label) + return tags if isinstance(tags, list) else [tags] + except ValueError: + pass + + if tag_mapping is not None: + tags = tag_mapping.get(label) + + if tags is not None: + return tags if isinstance(tags, list) else [tags] + + if key_mapping is not None: + key = key_mapping.get(label) + + if key is None: + key = fallback + + return [data.Tag(key=key, value=label)] + + +def label_from_tag( + tag: data.Tag, + label_fn: Optional[Callable[[data.Tag], str]] = None, + label_mapping: Optional[dict[data.Tag, str]] = None, + value_only: bool = False, + separator: str = ":", +) -> str: + """Convert a soundevent tag to a crowsetta label. + + This function facilitates the conversion of a soundevent tag into a + crowsetta label. Users can customize this conversion using the following + options: + + 1. If a custom mapping function (`label_fn` argument) is provided, it will + be used to directly convert the tag to a label. + 2. If a mapping dictionary (`label_mapping` argument) is provided, the + function will attempt to look up the label for the tag in the mapping. If + found, it returns the label; otherwise, it proceeds to the next option. + 3. If the `value_only` argument is set to True, the function returns only + the value of the tag. + 4. If none of the above conditions are met, the function constructs the + label by combining the tag's key and value with the specified separator. + + Parameters + ---------- + tag + The soundevent tag to convert to a label. + label_fn + A function to convert tags to labels. + label_mapping + A dictionary mapping tags to labels. + value_only + If True, return only the value of the tag, by default False. + separator + The separator to use between the key and value when constructing the + label, by default ":". + + Returns + ------- + str + The crowsetta label corresponding to the soundevent tag. + """ + if label_fn is not None: + return label_fn(tag) + + if label_mapping is not None: + label = label_mapping.get(tag) + + if label is not None: + return label + + if value_only: + return tag.value + + return f"{tag.key}{separator}{tag.value}" + + +def label_from_tags( + tags: Sequence[data.Tag], + seq_label_fn: Optional[Callable[[Sequence[data.Tag]], str]] = None, + select_by_key: Optional[str] = None, + index: Optional[int] = None, + separator: str = ",", + empty_label: str = EMPTY_LABEL, + **kwargs, +) -> str: + """Convert a sequence of soundevent tags to a crowsetta label. + + This function facilitates the conversion of a sequence of soundevent tags + into a Crowsetta label. Users can customize the conversion process using + the following options: + + 1. If a custom sequence label function (`seq_label_fn` argument) is + provided, it will be used to directly convert the sequence of tags to a + label. + 2. If the sequence of tags is empty, the function returns the specified + `empty_label`. + 3. If the `select_by_key` argument is provided, the function will + attempt to find the first tag in the sequence with a matching key. If + found, it returns the label converted from that tag using + `convert_tag_to_label`, otherwise it returns the `empty_label`. + 4. If the `index` argument is provided, it will be used to select a tag + from the sequence based on the index. If the index is out of bounds, it + wraps around to the valid range. The label converted from the selected + tag is then returned. + 5. If none of the above conditions are met, the function constructs a + label by joining the labels of all tags in the sequence with the + specified separator. + + Parameters + ---------- + tags + The sequence of soundevent tags to convert to a label. + seq_label_fn + A function to convert sequences of tags to labels. + select_by_key + If provided, select the first tag with a matching key for label + conversion. + index + If provided, use it as the index to select a tag from the sequence for + label conversion. The index is wrapped around if it exceeds the bounds. + separator + The separator to use between the labels when constructing the final + label. + empty_label + The label to return when the sequence of tags is empty. By default + "__empty__". + **kwargs + Additional keyword arguments passed to the `convert_tag_to_label` + function. + + Returns + ------- + str + The Crowsetta label corresponding to the sequence of soundevent tags. + """ + if seq_label_fn is not None: + return seq_label_fn(tags) + + if not tags: + return empty_label + + if select_by_key is not None: + tag = next((t for t in tags if t.key == select_by_key), None) + + if tag is None: + return empty_label + + return label_from_tag(tag, value_only=True, **kwargs) + + if index is not None: + index = index % len(tags) + return label_from_tag(tags[index], **kwargs) + + return separator.join([label_from_tag(tag, **kwargs) for tag in tags]) diff --git a/src/soundevent/io/crowsetta/segment.py b/src/soundevent/io/crowsetta/segment.py new file mode 100644 index 0000000..f674a03 --- /dev/null +++ b/src/soundevent/io/crowsetta/segment.py @@ -0,0 +1,171 @@ +"""crowsetta.segment module.""" + +from typing import List, Optional + +from crowsetta import Segment + +from soundevent import data +from soundevent.geometry import compute_bounds +from soundevent.io.crowsetta.labels import label_from_tags, label_to_tags + +__all__ = [ + "segment_from_annotation", + "segment_to_annotation", +] + + +def convert_geometry_to_interval( + geometry: data.Geometry, + cast_to_segment: bool = False, +) -> tuple[float, float]: + if geometry.type != "TimeInterval": + if not cast_to_segment: + raise ValueError( + "Cannot convert to a crowsetta segment " + "because the sound event geometry is not a TimeInterval." + ) + + start_time, _, end_time, _ = compute_bounds(geometry) + geometry = data.TimeInterval(coordinates=[start_time, end_time]) + + start_time, end_time = geometry.coordinates + return start_time, end_time + + +def convert_time_to_sample(recording: data.Recording, time: float) -> int: + return int(time * recording.samplerate) + + +def segment_from_annotation( + obj: data.SoundEventAnnotation, + cast_to_segment: bool = True, + **kwargs, +) -> Segment: + """Convert a soundevent annotation to a crowsetta segment. + + This function transforms a SoundEventAnnotation object into a Crowsetta + segment. The SoundEvent's geometry is used to determine the onset and + offset times of the segment, and the associated tags are converted into a + Crowsetta label using the `convert_tags_to_label` function. + + Parameters + ---------- + obj : data.SoundEventAnnotation + The SoundEventAnnotation object to convert. + cast_to_segment : bool, optional + If True, any geometry that is not a TimeInterval will be cast to a + TimeInterval, otherwise a ValueError will be raised. By default True. + **kwargs + Additional keyword arguments passed to the `convert_tags_to_label` + function. + + Returns + ------- + Segment + A Crowsetta Segment representing the converted soundevent segment, + with onset and offset times, sample indices, and associated label. + + Raises + ------ + ValueError + If the sound event has no geometry, or if the geometry is not a + TimeInterval and `cast_to_segment` is False. + """ + sound_event = obj.sound_event + geometry = sound_event.geometry + + if geometry is None: + raise ValueError( + "Cannot convert to a crowsetta segment " + "because the sound event has no geometry." + ) + + start_time, end_time = convert_geometry_to_interval( + geometry, + cast_to_segment, + ) + + start_sample = convert_time_to_sample(sound_event.recording, start_time) + end_sample = convert_time_to_sample(sound_event.recording, end_time) + label = label_from_tags(obj.tags, **kwargs) + + return Segment.from_keyword( + onset_s=start_time, + offset_s=end_time, + onset_sample=start_sample, + offset_sample=end_sample, + label=label, + ) + + +def segment_to_annotation( + segment: Segment, + recording: data.Recording, + adjust_time_expansion: bool = True, + notes: Optional[List[data.Note]] = None, + created_by: Optional[data.User] = None, + **kwargs, +) -> data.SoundEventAnnotation: + """Convert a crowsetta segment to a soundevent time interval annotation. + + This function transforms a Crowsetta segment into a SoundEvent time + interval annotation. The segment's onset and offset times are used to + create a time interval, and the label is converted into a list of + soundevent tags using the `convert_label_to_tags` function. + + Parameters + ---------- + segment : Segment + The Crowsetta segment to convert. + recording : data.Recording + The original recording associated with the segment. + adjust_time_expansion : bool, optional + If True, adjust the segment's onset and offset times based on the + recording's time expansion factor, by default True. + notes : List[data.Note], optional + Additional notes associated with the converted time interval, by + default None. + created_by : data.User, optional + User information representing the creator of annotation. By default + None. + **kwargs + Additional keyword arguments passed to the `convert_label_to_tags` + function. + + Returns + ------- + data.SoundEventAnnotation + A SoundEventAnnotation object representing the converted time interval, + containing a SoundEvent with the time interval, associated tags, + notes, and creator information. + """ + + if notes is None: + notes = [] + + start_time = segment.onset_s + end_time = segment.offset_s + + if adjust_time_expansion and recording.time_expansion != 1: + # NOTE: The time expansion factor is applied to the segment's onset and + # offset times to convert them to the original recording's time scale. + # This is necessary because the segment's onset and offset times are + # stored in the time scale of the expanded recording. + start_time = segment.onset_s / recording.time_expansion + end_time = segment.offset_s / recording.time_expansion + + geometry = data.TimeInterval(coordinates=[start_time, end_time]) + + tags = label_to_tags(segment.label, **kwargs) + + sound_event = data.SoundEvent( + geometry=geometry, + recording=recording, + ) + + return data.SoundEventAnnotation( + sound_event=sound_event, + tags=tags, + notes=notes, + created_by=created_by, + ) diff --git a/src/soundevent/io/crowsetta/sequence.py b/src/soundevent/io/crowsetta/sequence.py new file mode 100644 index 0000000..0dfd6f4 --- /dev/null +++ b/src/soundevent/io/crowsetta/sequence.py @@ -0,0 +1,122 @@ +from typing import List, Optional, Sequence + +import crowsetta + +from soundevent import data +from soundevent.io.crowsetta.segment import ( + segment_from_annotation, + segment_to_annotation, +) + +__all__ = [ + "sequence_from_annotations", + "sequence_to_annotations", +] + + +def sequence_from_annotations( + annotations: Sequence[data.SoundEventAnnotation], + cast_to_segment: bool = True, + ignore_errors: bool = False, + **kwargs, +) -> crowsetta.Sequence: + """Convert a sequence of soundevent annotations to a Crowsetta sequence. + + This function transforms a sequence of `SoundEventAnnotation` objects into + a Crowsetta sequence (`crowsetta.Sequence`). Each annotation is + individually converted into a Crowsetta segment using the + `to_crowsetta_segment` function. + + Parameters + ---------- + annotations : Sequence[data.SoundEventAnnotation] + The sequence of SoundEventAnnotation objects to convert. + cast_to_segment : bool, optional + If True, cast the annotations to Crowsetta segments, otherwise, keep + them as is, by default True. + ignore_errors : bool, optional + If True, ignore errors during conversion and continue with the next + annotation, otherwise this function will raise an error if an + annotation cannot be converted, by default False. + **kwargs + Additional keyword arguments passed to the `to_crowsetta_segment` + function. + + Returns + ------- + crowsetta.Sequence + A Crowsetta sequence representing the converted soundevent annotations. + + Raises + ------ + ValueError + If an annotation cannot be converted and `ignore_errors` is False. + """ + + segments = [] + + for annotation in annotations: + try: + segment = segment_from_annotation( + annotation, + cast_to_segment=cast_to_segment, + **kwargs, + ) + except ValueError as e: + if ignore_errors: + continue + + raise e + + segments.append(segment) + + return crowsetta.Sequence.from_segments(segments) + + +def sequence_to_annotations( + sequence: crowsetta.Sequence, + recording: data.Recording, + adjust_time_expansion: bool = True, + created_by: Optional[data.User] = None, + **kwargs, +) -> List[data.SoundEventAnnotation]: + """Convert a Crowsetta sequence to a list of soundevent annotations. + + This function transforms a Crowsetta sequence (`crowsetta.Sequence`) into a + list of `SoundEventAnnotation` objects. Each segment in the Crowsetta + sequence is individually converted into a soundevent annotation using the + `to_time_interval_annotation` function. + + Parameters + ---------- + sequence : crowsetta.Sequence + The Crowsetta sequence to convert. + recording : data.Recording + The original recording associated from which the Crowsetta sequence + was annotated. + adjust_time_expansion : bool, optional + If True, adjust the onset and offset times based on the recording's + time expansion factor, by default True. + created_by : data.User, optional + User information representing the creator of the converted soundevent + annotations, by default None. + **kwargs + Additional keyword arguments passed to the + `to_time_interval_annotation` function. + + Returns + ------- + List[data.SoundEventAnnotation] + A list of soundevent annotations representing the converted Crowsetta + sequence. + """ + return [ + segment_to_annotation( + segment, + recording, + adjust_time_expansion=adjust_time_expansion, + created_by=created_by, + **kwargs, + ) + for segment in sequence.segments + ] diff --git a/tests/test_io/conftest.py b/tests/test_io/conftest.py index bf98976..b4b445c 100644 --- a/tests/test_io/conftest.py +++ b/tests/test_io/conftest.py @@ -40,6 +40,30 @@ def recording( ], ) +@pytest.fixture +def time_expanded_recording( + random_wav: Callable[[], Path], + user: data.User, + tags: List[data.Tag], + note: data.Note, +) -> data.Recording: + path = random_wav() + return data.Recording.from_file( + path, + date=datetime.date(2020, 1, 1), + time=datetime.time(12, 0, 0), + time_expansion=10, + latitude=1.0, + longitude=2.0, + owners=[user], + rights="CC BY 4.0", + tags=tags, + notes=[note], + features=[ + data.Feature(name="MaxAmp", value=23.3), + ], + ) + @pytest.fixture def recording_set(recording: data.Recording) -> data.RecordingSet: diff --git a/tests/test_io/test_crowsetta/__init__.py b/tests/test_io/test_crowsetta/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_io/test_crowsetta/test_annotation.py b/tests/test_io/test_crowsetta/test_annotation.py new file mode 100644 index 0000000..df80ac7 --- /dev/null +++ b/tests/test_io/test_crowsetta/test_annotation.py @@ -0,0 +1,318 @@ +"""Test suite for the soundevent.io.crowsetta.annotation module""" + +import datetime +from pathlib import Path + +import crowsetta +import pytest + +from soundevent import data, io + + +@pytest.fixture +def clip_annotation(recording: data.Recording) -> data.ClipAnnotation: + return data.ClipAnnotation( + clip=data.Clip( + recording=recording, + start_time=0.0, + end_time=1.0, + ), + tags=[ + data.Tag(key="animal", value="dog"), + ], + sound_events=[ + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.TimeInterval(coordinates=[0.5, 1.5]), + features=[data.Feature(name="test", value=1.0)], + ), + tags=[data.Tag(key="animal", value="dog")], + notes=[data.Note(message="random note")], + ), + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.BoundingBox( + coordinates=[0.5, 0.5, 1.5, 1.5] + ), + features=[data.Feature(name="test", value=1.0)], + ), + tags=[data.Tag(key="animal", value="cat")], + notes=[data.Note(message="random note")], + ), + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.LineString( + coordinates=[[0.5, 0.5], [1.5, 1.5]] + ), + features=[data.Feature(name="test", value=1.0)], + ), + tags=[data.Tag(key="animal", value="cat")], + notes=[data.Note(message="random note")], + ), + ], + notes=[data.Note(message="random note")], + ) + + +@pytest.fixture +def sequence_annotation( + tmp_path: Path, + recording: data.Recording, +) -> crowsetta.Annotation: + return crowsetta.Annotation( + annot_path=tmp_path / "annotation.txt", + notated_path=recording.path, + seq=crowsetta.Sequence.from_segments( + [ + crowsetta.Segment.from_keyword( + label="dog", + onset_s=0.5, + offset_s=1.5, + ), + crowsetta.Segment.from_keyword( + label="cat", + onset_s=2.0, + offset_s=2.5, + ), + ] + ), + ) + + +@pytest.fixture +def bbox_annotation( + tmp_path: Path, + recording: data.Recording, +) -> crowsetta.Annotation: + return crowsetta.Annotation( + annot_path=tmp_path / "annotation.txt", + notated_path=recording.path, + bboxes=[ + crowsetta.BBox( + onset=0.5, + low_freq=0.5, + high_freq=1.5, + offset=1.5, + label="dog", + ), + crowsetta.BBox( + onset=2.0, + low_freq=0.5, + high_freq=1.5, + offset=2.5, + label="cat", + ), + ], + ) + + +def test_seq_annotation_from_clip_annotation( + clip_annotation: data.ClipAnnotation, + tmp_path: Path, +): + annotation = io.crowsetta.annotation_from_clip_annotation( + clip_annotation, + annot_path=tmp_path / "annotation.txt", + annotation_fmt="seq", + ) + assert isinstance(annotation, crowsetta.Annotation) + assert isinstance(annotation.seq, crowsetta.Sequence) + assert len(annotation.seq.segments) == 3 + + +def test_bbox_annotation_from_clip_annotation( + clip_annotation: data.ClipAnnotation, + tmp_path: Path, +): + annotation = io.crowsetta.annotation_from_clip_annotation( + clip_annotation, + annot_path=tmp_path / "annotation.txt", + annotation_fmt="bbox", + ) + assert isinstance(annotation, crowsetta.Annotation) + assert isinstance(annotation.bboxes, list) + assert all(isinstance(bbox, crowsetta.BBox) for bbox in annotation.bboxes) + # NOTE: there should only be 2 bboxes, not 3, because the TimeInterval + # are not converted to BBox by default, and errors are ignored. + assert len(annotation.bboxes) == 2 + + +def test_annotation_from_clip_annotation_fails_with_unknown_fmt( + clip_annotation: data.ClipAnnotation, + tmp_path: Path, +): + with pytest.raises(ValueError): + io.crowsetta.annotation_from_clip_annotation( + clip_annotation, + annot_path=tmp_path / "annotation.txt", + annotation_fmt="unknown", # type: ignore + ) + + +def test_annotation_to_clip_annotation_from_seq( + sequence_annotation: crowsetta.Annotation, +): + annotation = io.crowsetta.annotation_to_clip_annotation( + sequence_annotation, + ) + assert isinstance(annotation, data.ClipAnnotation) + assert isinstance(annotation.clip, data.Clip) + assert len(annotation.sound_events) == 2 + + +def test_annotation_to_clip_annotation_from_bbox( + bbox_annotation: crowsetta.Annotation, +): + annotation = io.crowsetta.annotation_to_clip_annotation( + bbox_annotation, + ) + assert isinstance(annotation, data.ClipAnnotation) + assert isinstance(annotation.clip, data.Clip) + assert len(annotation.sound_events) == 2 + + +def test_annotation_to_clip_annotation_with_notes_and_tags( + sequence_annotation: crowsetta.Annotation, + user: data.User, +): + tags = [ + data.Tag(key="soundscape", value="garden"), + ] + annotation = io.crowsetta.annotation_to_clip_annotation( + sequence_annotation, + tags=tags, + notes=[data.Note(message="random note")], + created_by=user, + ) + assert isinstance(annotation, data.ClipAnnotation) + assert isinstance(annotation.clip, data.Clip) + assert len(annotation.sound_events) == 2 + + +def test_annotation_to_clip_annotation_with_recording( + sequence_annotation: crowsetta.Annotation, + recording: data.Recording, +): + recording = recording.model_copy( + update=dict( + latitude=10, + longitude=10, + date=datetime.date(2020, 1, 1), + time=datetime.time(12, 0, 0), + ) + ) + annotation = io.crowsetta.annotation_to_clip_annotation( + sequence_annotation, + recording=recording, + ) + assert isinstance(annotation, data.ClipAnnotation) + assert isinstance(annotation.clip, data.Clip) + assert len(annotation.sound_events) == 2 + assert annotation.clip.recording == recording + + +def test_annotation_to_clip_annotation_with_recording_kwargs( + sequence_annotation: crowsetta.Annotation, +): + annotation = io.crowsetta.annotation_to_clip_annotation( + sequence_annotation, + recording_kwargs=dict( + latitude=10, + longitude=10, + date=datetime.date(2020, 1, 1), + time=datetime.time(12, 0, 0), + ), + ) + assert isinstance(annotation, data.ClipAnnotation) + assert isinstance(annotation.clip, data.Clip) + assert len(annotation.sound_events) == 2 + assert annotation.clip.recording.latitude == 10 + assert annotation.clip.recording.longitude == 10 + assert annotation.clip.recording.date == datetime.date(2020, 1, 1) + assert annotation.clip.recording.time == datetime.time(12, 0, 0) + + +def test_annotation_to_clip_annotation_fails_without_path(tmp_path: Path): + annotation = crowsetta.Annotation( + annot_path=tmp_path / "annotation.txt", + seq=crowsetta.Sequence.from_segments( + [ + crowsetta.Segment.from_keyword( + label="dog", + onset_s=0.5, + offset_s=1.5, + ), + crowsetta.Segment.from_keyword( + label="cat", + onset_s=2.0, + offset_s=2.5, + ), + ] + ), + ) + + with pytest.raises(ValueError): + io.crowsetta.annotation_to_clip_annotation(annotation) + + +def test_annotation_to_clp_annotation_fails_if_paths_dont_match( + recording: data.Recording, + tmp_path: Path, +): + annotation = crowsetta.Annotation( + annot_path=tmp_path / "annotation.txt", + notated_path=tmp_path / "non_existent.wav", + seq=crowsetta.Sequence.from_segments( + [ + crowsetta.Segment.from_keyword( + label="dog", + onset_s=0.5, + offset_s=1.5, + ), + crowsetta.Segment.from_keyword( + label="cat", + onset_s=2.0, + offset_s=2.5, + ), + ] + ), + ) + assert recording.path != annotation.notated_path + + with pytest.raises(ValueError): + io.crowsetta.annotation_to_clip_annotation( + annotation, + recording=recording, + ) + + +def test_bbox_annotation_from_clip_annotation_with_incompatible_geoms( + clip_annotation: data.ClipAnnotation, + tmp_path: Path, +): + bbox_annotation = io.crowsetta.annotation_from_clip_annotation( + clip_annotation, + annot_path=tmp_path / "annotation.txt", + annotation_fmt="bbox", + cast_geometry=False, + ) + assert isinstance(bbox_annotation, crowsetta.Annotation) + assert isinstance(bbox_annotation.bboxes, list) + assert len(bbox_annotation.bboxes) == 1 + + +def test_bbox_annotation_from_clip_annotation_fails_on_incompatible_geoms( + clip_annotation: data.ClipAnnotation, + tmp_path: Path, +): + with pytest.raises(ValueError): + io.crowsetta.annotation_from_clip_annotation( + clip_annotation, + annot_path=tmp_path / "annotation.txt", + annotation_fmt="bbox", + cast_geometry=False, + ignore_errors=False, + ) diff --git a/tests/test_io/test_crowsetta/test_bbox.py b/tests/test_io/test_crowsetta/test_bbox.py new file mode 100644 index 0000000..4cf2573 --- /dev/null +++ b/tests/test_io/test_crowsetta/test_bbox.py @@ -0,0 +1,200 @@ +"""Test suite for the soundevent.io.crowsetta.bbox module""" + +import crowsetta +import pytest + +from soundevent import data, io + + +@pytest.fixture +def bounding_box() -> data.BoundingBox: + return data.BoundingBox( + coordinates=[0.5, 0.5, 1.5, 1.5], + ) + + +@pytest.fixture +def sound_event( + recording: data.Recording, bounding_box: data.BoundingBox +) -> data.SoundEvent: + return data.SoundEvent( + geometry=bounding_box, + recording=recording, + features=[data.Feature(name="test", value=1.0)], + ) + + +@pytest.fixture +def sound_event_annotation( + user: data.User, + sound_event: data.SoundEvent, +) -> data.SoundEventAnnotation: + return data.SoundEventAnnotation( + sound_event=sound_event, + notes=[data.Note(message="random note", created_by=user)], + tags=[ + data.Tag(key="animal", value="dog"), + ], + ) + + +@pytest.fixture +def bbox() -> crowsetta.BBox: + return crowsetta.BBox( + onset=0.5, + low_freq=0.5, + high_freq=1.5, + offset=1.5, + label="dog", + ) + + +def test_bbox_from_annotation( + sound_event_annotation: data.SoundEventAnnotation, +): + bbox = io.crowsetta.bbox_from_annotation(sound_event_annotation) + assert isinstance(bbox, crowsetta.BBox) + assert bbox.onset == 0.5 + assert bbox.low_freq == 0.5 + assert bbox.high_freq == 1.5 + assert bbox.offset == 1.5 + assert bbox.label == "animal:dog" + + +def test_bbox_from_annotation_fails_on_other_geometries( + sound_event_annotation: data.SoundEventAnnotation, +): + sound_event_annotation.sound_event.geometry = data.LineString( + coordinates=[[0.5, 0.5], [1.5, 1.5]], + ) + + with pytest.raises(ValueError): + io.crowsetta.bbox_from_annotation( + sound_event_annotation, + cast_to_bbox=False, + ) + + +def test_bbox_from_annotation_fails_on_empty_geometry( + recording: data.Recording, +): + sound_event_annotation = data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=None, + ), + ) + + with pytest.raises(ValueError): + io.crowsetta.bbox_from_annotation( + sound_event_annotation, + cast_to_bbox=False, + ) + + +def test_bbox_from_annotations_can_cast_to_bbox( + sound_event_annotation: data.SoundEventAnnotation, +): + sound_event_annotation.sound_event.geometry = data.LineString( + coordinates=[[0.5, 0.5], [1.5, 1.5]], + ) + + bbox = io.crowsetta.bbox_from_annotation( + sound_event_annotation, + cast_to_bbox=True, + ) + assert isinstance(bbox, crowsetta.BBox) + assert bbox.onset == 0.5 + assert bbox.low_freq == 0.5 + assert bbox.high_freq == 1.5 + assert bbox.offset == 1.5 + assert bbox.label == "animal:dog" + + +def test_bbox_from_annotation_fails_on_time_geometries( + sound_event_annotation: data.SoundEventAnnotation, +): + sound_event_annotation.sound_event.geometry = data.TimeInterval( + coordinates=[0.5, 1.5], + ) + + with pytest.raises(ValueError): + io.crowsetta.bbox_from_annotation( + sound_event_annotation, + cast_to_bbox=True, + ) + + +def test_bbox_from_annotation_cast_time_geometries( + sound_event_annotation: data.SoundEventAnnotation, +): + sound_event_annotation.sound_event.geometry = data.TimeInterval( + coordinates=[0.5, 1.5], + ) + + bbox = io.crowsetta.bbox_from_annotation( + sound_event_annotation, + cast_to_bbox=True, + raise_on_time_geometries=False, + ) + nyquist = sound_event_annotation.sound_event.recording.samplerate / 2 + assert isinstance(bbox, crowsetta.BBox) + assert bbox.onset == 0.5 + assert bbox.low_freq == 0 + assert bbox.high_freq == nyquist + assert bbox.offset == 1.5 + assert bbox.label == "animal:dog" + + +def test_bbox_to_annotation( + bbox: crowsetta.BBox, + recording: data.Recording, +): + annotation = io.crowsetta.bbox_to_annotation(bbox, recording) + assert isinstance(annotation, data.SoundEventAnnotation) + geometry = annotation.sound_event.geometry + assert isinstance(geometry, data.BoundingBox) + assert geometry.coordinates == [0.5, 0.5, 1.5, 1.5] + assert annotation.tags == [data.Tag(key="crowsetta", value="dog")] + + +def test_bbox_to_annotation_with_notes_and_created_by( + bbox: crowsetta.BBox, + recording: data.Recording, + user: data.User, +): + note = data.Note( + message="random note", + created_by=user, + ) + annotation = io.crowsetta.bbox_to_annotation( + bbox, recording, notes=[note], created_by=user + ) + assert annotation.notes == [note] + assert annotation.created_by == user + + +def test_bbox_to_annotation_with_time_expanded_recording( + bbox: crowsetta.BBox, + time_expanded_recording: data.Recording, +): + annotation = io.crowsetta.bbox_to_annotation(bbox, time_expanded_recording) + assert annotation.sound_event.recording == time_expanded_recording + geometry = annotation.sound_event.geometry + assert isinstance(geometry, data.BoundingBox) + assert tuple(geometry.coordinates) == (0.05, 5.0, 0.15, 15.0) + + +def test_bbox_to_annotation_with_time_expanded_recording_no_adjustment( + bbox: crowsetta.BBox, + time_expanded_recording: data.Recording, +): + annotation = io.crowsetta.bbox_to_annotation( + bbox, + time_expanded_recording, + adjust_time_expansion=False, + ) + assert annotation.sound_event.recording == time_expanded_recording + geometry = annotation.sound_event.geometry + assert isinstance(geometry, data.BoundingBox) + assert tuple(geometry.coordinates) == (0.5, 0.5, 1.5, 1.5) diff --git a/tests/test_io/test_crowsetta/test_labels.py b/tests/test_io/test_crowsetta/test_labels.py new file mode 100644 index 0000000..9e5a86d --- /dev/null +++ b/tests/test_io/test_crowsetta/test_labels.py @@ -0,0 +1,266 @@ +"""Test Suite for the soundevent.io.crowsetta.labels module""" + +from soundevent import data, io + + +def test_label_from_tag_without_kwargs(): + tag = data.Tag(key="animal", value="dog") + label = io.crowsetta.label_from_tag(tag) + assert label == "animal:dog" + + +def test_label_from_tag_with_only_value(): + tag = data.Tag(key="animal", value="dog") + label = io.crowsetta.label_from_tag(tag, value_only=True) + assert label == "dog" + + +def test_label_from_tag_with_label_mapping(): + tag = data.Tag(key="crowsetta", value="dog") + label = io.crowsetta.label_from_tag( + tag, + label_mapping={data.Tag(key="crowsetta", value="dog"): "cat"}, + ) + assert label == "cat" + + +def test_label_from_tag_with_label_mapping_missing_label(): + tag = data.Tag(key="crowsetta", value="dog") + label = io.crowsetta.label_from_tag( + tag, + label_mapping={data.Tag(key="crowsetta", value="cat"): "dog"}, + ) + assert label == "crowsetta:dog" + + +def test_label_from_tag_with_label_fn(): + tag = data.Tag(key="crowsetta", value="dog") + label = io.crowsetta.label_from_tag( + tag, + label_fn=lambda _: "cat", + ) + assert label == "cat" + + +def test_label_from_tag_with_label_fn_and_label_mapping(): + tag = data.Tag(key="crowsetta", value="dog") + label = io.crowsetta.label_from_tag( + tag, + label_fn=lambda _: "cat", # type: ignore + label_mapping={data.Tag(key="crowsetta", value="dog"): "bird"}, + ) + assert label == "cat" + + +def test_label_to_tags_with_custom_fn(): + label = "crowsetta-dog" + tag = io.crowsetta.label_to_tags( + label, + tag_fn=lambda _: data.Tag(key="crowsetta", value="dog"), + ) + assert tag == [data.Tag(key="crowsetta", value="dog")] + + +def test_label_to_tags_with_custom_fn_list(): + label = "crowsetta-dog" + tag = io.crowsetta.label_to_tags( + label, + tag_fn=lambda _: [data.Tag(key="crowsetta", value="dog")], + ) + assert tag == [data.Tag(key="crowsetta", value="dog")] + + +def test_label_to_tags_with_failing_custom_fn(): + def failing_fn(label): + if label == "dog": + raise ValueError("dog is not a valid label") + return data.Tag(key="animal", value=label) + + label = "dog" + tag = io.crowsetta.label_to_tags( + label, + tag_fn=failing_fn, + ) + assert tag == [data.Tag(key="crowsetta", value="dog")] + + +def test_label_to_tags_with_tag_mapping_single_tag(): + label = "crowsetta-dog" + tag = io.crowsetta.label_to_tags( + label, + tag_mapping={"crowsetta-dog": data.Tag(key="crowsetta", value="dog")}, + ) + assert tag == [data.Tag(key="crowsetta", value="dog")] + + +def test_label_to_tags_with_tag_mapping_tag_list(): + label = "crowsetta-dog" + tag = io.crowsetta.label_to_tags( + label, + tag_mapping={"crowsetta-dog": [data.Tag(key="animal", value="dog")]}, + ) + assert tag == [data.Tag(key="animal", value="dog")] + + +def test_label_to_tags_with_tag_mapping_missing_label(): + label = "dog" + tag = io.crowsetta.label_to_tags( + label, + tag_mapping={"cat": data.Tag(key="animal", value="cat")}, + ) + assert tag == [data.Tag(key="crowsetta", value="dog")] + + +def test_label_to_tags_with_key_mapping(): + key_mapping = {"bat": "animal", "female": "sex"} + tag = io.crowsetta.label_to_tags( + "bat", + key_mapping=key_mapping, + ) + assert tag == [data.Tag(key="animal", value="bat")] + + tag = io.crowsetta.label_to_tags( + "female", + key_mapping=key_mapping, + ) + assert tag == [data.Tag(key="sex", value="female")] + + tags = io.crowsetta.label_to_tags( + "large", + key_mapping=key_mapping, + ) + + assert tags == [data.Tag(key="crowsetta", value="large")] + + +def test_label_to_tags_with_key_mapping_fallback(): + key_mapping = {"bat": "animal"} + tag = io.crowsetta.label_to_tags( + "dog", key_mapping=key_mapping, fallback="pet" + ) + assert tag == [data.Tag(key="pet", value="dog")] + + +def test_label_to_tags_with_empty_labels(): + tags = io.crowsetta.label_to_tags( + "__empty__", + ) + assert tags == [] + + tags = io.crowsetta.label_to_tags( + "NA", + empty_labels=["NA"], + ) + assert tags == [] + + +def test_label_from_tags_with_custom_fn(): + tags = [data.Tag(key="animal", value="cat")] + label = io.crowsetta.label_from_tags( + tags, + seq_label_fn=lambda _: "dog", + ) + assert label == "dog" + + +def test_label_from_tags_empty_list(): + label = io.crowsetta.label_from_tags([]) + assert label == "__empty__" + + label = io.crowsetta.label_from_tags([], empty_label="NA") + assert label == "NA" + + +def test_label_from_tags_select_by_key(): + tags = [ + data.Tag(key="animal", value="dog"), + data.Tag(key="sex", value="male"), + ] + label = io.crowsetta.label_from_tags( + tags, + select_by_key="animal", + ) + assert label == "dog" + + label = io.crowsetta.label_from_tags( + tags, + select_by_key="sex", + ) + assert label == "male" + + +def test_label_from_tags_select_by_key_missing_key(): + tags = [ + data.Tag(key="animal", value="dog"), + ] + label = io.crowsetta.label_from_tags( + tags, + select_by_key="sex", + ) + assert label == "__empty__" + + label = io.crowsetta.label_from_tags( + tags, + select_by_key="sex", + empty_label="NA", + ) + assert label == "NA" + + +def test_label_from_tags_select_by_index(): + tags = [ + data.Tag(key="animal", value="dog"), + data.Tag(key="sex", value="male"), + ] + label = io.crowsetta.label_from_tags( + tags, + index=0, + value_only=True, + ) + assert label == "dog" + + label = io.crowsetta.label_from_tags( + tags, + index=1, + value_only=False, + ) + assert label == "sex:male" + + label = io.crowsetta.label_from_tags( + tags, + index=2, + value_only=False, + ) + assert label == "animal:dog" + + label = io.crowsetta.label_from_tags( + tags, + index=-1, + value_only=True, + ) + assert label == "male" + + +def test_label_from_tags_concat_all(): + tags = [ + data.Tag(key="animal", value="dog"), + data.Tag(key="sex", value="male"), + ] + + label = io.crowsetta.label_from_tags( + tags, + ) + assert label == "animal:dog,sex:male" + + +def test_label_from_tags_with_custom_separator(): + tags = [ + data.Tag(key="animal", value="dog"), + data.Tag(key="sex", value="male"), + ] + + label = io.crowsetta.label_from_tags( + tags, + separator="|", + ) + assert label == "animal:dog|sex:male" diff --git a/tests/test_io/test_crowsetta/test_segments.py b/tests/test_io/test_crowsetta/test_segments.py new file mode 100644 index 0000000..c7d8c4a --- /dev/null +++ b/tests/test_io/test_crowsetta/test_segments.py @@ -0,0 +1,160 @@ +"""Test suite for the soundevent.io.crowsetta.segments module""" + +import crowsetta +import pytest + +from soundevent import data, io + + +@pytest.fixture +def interval() -> data.TimeInterval: + return data.TimeInterval(coordinates=[0.5, 1.5]) + + +@pytest.fixture +def sound_event( + recording: data.Recording, interval: data.TimeInterval +) -> data.SoundEvent: + return data.SoundEvent( + geometry=interval, + recording=recording, + features=[data.Feature(name="test", value=1.0)], + ) + + +@pytest.fixture +def sound_event_annotation( + user: data.User, + sound_event: data.SoundEvent, +) -> data.SoundEventAnnotation: + return data.SoundEventAnnotation( + sound_event=sound_event, + notes=[data.Note(message="random note", created_by=user)], + tags=[ + data.Tag(key="animal", value="dog"), + ], + ) + + +@pytest.fixture +def segment() -> crowsetta.Segment: + return crowsetta.Segment.from_keyword( + label="dog", + onset_s=0.5, + offset_s=1.5, + ) + + +def test_segment_from_annotation_fails_on_empty_geometry( + recording: data.Recording, +): + sound_event_annotation = data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=None, + ), + ) + + with pytest.raises(ValueError): + io.crowsetta.segment_from_annotation(sound_event_annotation) + + +def test_segment_from_annotation( + sound_event_annotation: data.SoundEventAnnotation, +): + segment = io.crowsetta.segment_from_annotation(sound_event_annotation) + assert isinstance(segment, crowsetta.Segment) + assert segment.onset_s == 0.5 + assert segment.offset_s == 1.5 + assert segment.label == "animal:dog" + + +def test_segment_from_annotation_fails_if_not_a_time_interval( + sound_event_annotation: data.SoundEventAnnotation, +): + sound_event_annotation.sound_event.geometry = data.Point( + coordinates=[0.5, 1] + ) + with pytest.raises(ValueError): + io.crowsetta.segment_from_annotation( + sound_event_annotation, + cast_to_segment=False, + ) + + +def test_segment_from_annotation_casts_to_segment( + sound_event_annotation: data.SoundEventAnnotation, +): + sound_event_annotation.sound_event.geometry = data.Point( + coordinates=[0.5, 1] + ) + segment = io.crowsetta.segment_from_annotation( + sound_event_annotation, + cast_to_segment=True, + ) + assert isinstance(segment, crowsetta.Segment) + assert segment.onset_s == 0.5 + assert segment.offset_s == 0.5 + assert segment.label == "animal:dog" + + +def test_segment_to_annotation( + segment: crowsetta.Segment, + recording: data.Recording, +): + annotation = io.crowsetta.segment_to_annotation(segment, recording) + assert isinstance(annotation, data.SoundEventAnnotation) + geometry = annotation.sound_event.geometry + assert isinstance(geometry, data.TimeInterval) + assert tuple(geometry.coordinates) == (0.5, 1.5) + assert annotation.sound_event.recording == recording + assert annotation.tags == [data.Tag(key="crowsetta", value="dog")] + + +def test_segment_to_annotation_with_notes_and_created_by( + segment: crowsetta.Segment, + recording: data.Recording, + user: data.User, +): + note = data.Note( + message="random note", + created_by=user, + ) + annotation = io.crowsetta.segment_to_annotation( + segment, + recording, + created_by=user, + notes=[note], + ) + assert isinstance(annotation, data.SoundEventAnnotation) + assert annotation.notes == [note] + assert annotation.created_by == user + + +def test_segment_to_annotation_on_time_expanded_recording( + segment: crowsetta.Segment, + time_expanded_recording: data.Recording, +): + annotation = io.crowsetta.segment_to_annotation( + segment, + time_expanded_recording, + ) + assert isinstance(annotation, data.SoundEventAnnotation) + geometry = annotation.sound_event.geometry + assert isinstance(geometry, data.TimeInterval) + assert tuple(geometry.coordinates) == (0.05, 0.15) + + +def test_segment_to_annotation_on_time_expanded_recording_without_adjustment( + segment: crowsetta.Segment, + time_expanded_recording: data.Recording, +): + annotation = io.crowsetta.segment_to_annotation( + segment, + time_expanded_recording, + adjust_time_expansion=False, + ) + assert isinstance(annotation, data.SoundEventAnnotation) + geometry = annotation.sound_event.geometry + assert isinstance(geometry, data.TimeInterval) + assert tuple(geometry.coordinates) == (0.5, 1.5) diff --git a/tests/test_io/test_crowsetta/test_sequence.py b/tests/test_io/test_crowsetta/test_sequence.py new file mode 100644 index 0000000..0544aca --- /dev/null +++ b/tests/test_io/test_crowsetta/test_sequence.py @@ -0,0 +1,172 @@ +"""Test suite for the soundevent.io.crowsetta.sequence module""" + +from typing import List + +import crowsetta +import numpy as np +import pytest + +from soundevent import data, io + + +@pytest.fixture +def intervals() -> List[data.TimeInterval]: + return [ + data.TimeInterval(coordinates=[0.5, 1.5]), + data.TimeInterval(coordinates=[2.0, 2.5]), + ] + + +@pytest.fixture +def sound_events( + recording: data.Recording, + intervals: List[data.TimeInterval], +) -> List[data.SoundEvent]: + return [ + data.SoundEvent( + geometry=interval, + recording=recording, + features=[data.Feature(name="test", value=1.0)], + ) + for interval in intervals + ] + + +@pytest.fixture +def sound_event_annotations( + user: data.User, + sound_events: List[data.SoundEvent], +) -> List[data.SoundEventAnnotation]: + tags = [ + data.Tag(key="animal", value="dog"), + data.Tag(key="animal", value="cat"), + ] + return [ + data.SoundEventAnnotation( + sound_event=sound_event, + notes=[data.Note(message="random note", created_by=user)], + tags=[tag], + ) + for sound_event, tag in zip(sound_events, tags) + ] + + +@pytest.fixture +def sequence() -> crowsetta.Sequence: + return crowsetta.Sequence.from_segments( + [ + crowsetta.Segment.from_keyword( + label="dog", + onset_s=0.5, + offset_s=1.5, + ), + crowsetta.Segment.from_keyword( + label="cat", + onset_s=2.0, + offset_s=2.5, + ), + ], + ) + + +def test_sequence_from_annotations( + sound_event_annotations: List[data.SoundEventAnnotation], +): + sequence = io.crowsetta.sequence_from_annotations(sound_event_annotations) + assert isinstance(sequence, crowsetta.Sequence) + assert (sequence.onsets_s == np.array([0.5, 2.0])).all() + assert (sequence.offsets_s == np.array([1.5, 2.5])).all() + assert (sequence.labels == np.array(["animal:dog", "animal:cat"])).all() + + +def test_sequence_from_annotations_fails_on_non_compatible_geometries( + recording: data.Recording, +): + sound_event_annotations = [ + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.LineString(coordinates=[[0.5, 0.5], [1.5, 1.5]]), + ), + ), + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.TimeInterval(coordinates=[0.5, 1.5]) + ), + ), + ] + + with pytest.raises(ValueError): + io.crowsetta.sequence_from_annotations( + sound_event_annotations, + cast_to_segment=False, + ) + + +def test_sequence_from_annotations_ignores_non_compatible_geometries( + recording: data.Recording, +): + sound_event_annotations = [ + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.LineString(coordinates=[[0.5, 0.5], [1.5, 1.5]]), + ), + ), + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.TimeInterval(coordinates=[0.5, 1.5]) + ), + ), + ] + + sequence = io.crowsetta.sequence_from_annotations( + sound_event_annotations, + cast_to_segment=False, + ignore_errors=True, + ) + assert isinstance(sequence, crowsetta.Sequence) + assert len(sequence.segments) == 1 + + +def test_sequence_from_annotation_casts_to_geometry( + recording: data.Recording, +): + sound_event_annotations = [ + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.LineString(coordinates=[[0.5, 0.5], [1.5, 1.5]]), + ), + ), + data.SoundEventAnnotation( + sound_event=data.SoundEvent( + recording=recording, + geometry=data.TimeInterval(coordinates=[0.5, 1.5]) + ), + ), + ] + + sequence = io.crowsetta.sequence_from_annotations( + sound_event_annotations, + cast_to_segment=True, + ) + assert isinstance(sequence, crowsetta.Sequence) + assert len(sequence.segments) == 2 + assert (sequence.onsets_s == np.array([0.5, 0.5])).all() + assert (sequence.offsets_s == np.array([1.5, 1.5])).all() + + +def test_sequence_to_annotations( + sequence: crowsetta.Sequence, + recording: data.Recording, +): + annotations = io.crowsetta.sequence_to_annotations( + sequence, + recording, + ) + assert len(annotations) == 2 + assert all(isinstance(ann, data.SoundEventAnnotation) for ann in annotations) + assert all(ann.sound_event.recording == recording for ann in annotations) From 182a94b5c897e6085119eac973175487aec9523b Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 19:53:38 +0000 Subject: [PATCH 02/10] Added test that import example files provided by crowsetta --- pyproject.toml | 9 +-- src/soundevent/io/crowsetta/segment.py | 26 +++++++- tests/test_io/test_crowsetta/test_import.py | 65 +++++++++++++++++++ tests/test_io/test_crowsetta/test_segments.py | 16 +++++ 4 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 tests/test_io/test_crowsetta/test_import.py diff --git a/pyproject.toml b/pyproject.toml index f6c51d4..ada1f98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,18 +18,15 @@ audio = [ "cython>=0.29.36", "soundfile>=0.12.1", ] -evaluation = [ - "scikit-learn>=1.3.2", -] +evaluation = ["scikit-learn>=1.3.2"] geometry = ["shapely>=2.0.1"] +crowsetta = ["crowsetta>=4.0.0.post2"] all = [ "soundevent[plot]", "soundevent[audio]", "soundevent[evaluation]", "soundevent[geometry]", -] -crowsetta = [ - "crowsetta>=4.0.0.post2", + "soundevent[crowsetta]", ] [build-system] diff --git a/src/soundevent/io/crowsetta/segment.py b/src/soundevent/io/crowsetta/segment.py index f674a03..531dfe1 100644 --- a/src/soundevent/io/crowsetta/segment.py +++ b/src/soundevent/io/crowsetta/segment.py @@ -146,13 +146,35 @@ def segment_to_annotation( start_time = segment.onset_s end_time = segment.offset_s + if start_time is None: + samplerate = recording.samplerate / recording.time_expansion + + if segment.onset_sample is None: + raise ValueError( + "Cannot convert to a soundevent annotation " + "because the segment has no onset time." + ) + + start_time = segment.onset_sample / samplerate + + if end_time is None: + samplerate = recording.samplerate / recording.time_expansion + + if segment.offset_sample is None: + raise ValueError( + "Cannot convert to a soundevent annotation " + "because the segment has no offset time." + ) + + end_time = segment.offset_sample / samplerate + if adjust_time_expansion and recording.time_expansion != 1: # NOTE: The time expansion factor is applied to the segment's onset and # offset times to convert them to the original recording's time scale. # This is necessary because the segment's onset and offset times are # stored in the time scale of the expanded recording. - start_time = segment.onset_s / recording.time_expansion - end_time = segment.offset_s / recording.time_expansion + start_time = start_time / recording.time_expansion + end_time = end_time / recording.time_expansion geometry = data.TimeInterval(coordinates=[start_time, end_time]) diff --git a/tests/test_io/test_crowsetta/test_import.py b/tests/test_io/test_crowsetta/test_import.py new file mode 100644 index 0000000..555f327 --- /dev/null +++ b/tests/test_io/test_crowsetta/test_import.py @@ -0,0 +1,65 @@ +import warnings + +warnings.filterwarnings("ignore", category=UserWarning, module="crowsetta") +from pathlib import Path + +import crowsetta + +from soundevent import data, io + + +def test_can_import_all_example_formats( + tmp_path: Path, + recording: data.Recording, +): + data_dir = tmp_path / "crowsetta" + crowsetta.data.extract_data_files(user_data_dir=data_dir) + + formats = crowsetta.formats.as_list() + + for fmt in formats: + try: + example = crowsetta.data.get(fmt, user_data_dir=data_dir) + except ValueError: + continue + + scribe = crowsetta.Transcriber(fmt) + + from_file_kwargs = {} + to_annot_kwargs = {} + + if fmt == "raven": + from_file_kwargs = {"annot_col": "Species"} + + elif fmt == "simple-seq": + from_file_kwargs = dict( + columns_map={ + "start_seconds": "onset_s", + "stop_seconds": "offset_s", + "name": "label", + }, + read_csv_kwargs={"index_col": 0}, + ) + elif fmt == "timit": + from_file_kwargs = {"audio_path": recording.path} + to_annot_kwargs = {"samplerate": recording.samplerate} + + annotation = scribe.from_file( + example.annot_path, **from_file_kwargs + ).to_annot(**to_annot_kwargs) + + if isinstance(annotation, list): + annotation = annotation[0] + + assert isinstance(annotation, crowsetta.Annotation) + + if annotation.notated_path is not None: + recording = recording.model_copy( + update=dict(path=annotation.notated_path) + ) + + clip_annotation = io.crowsetta.annotation_to_clip_annotation( + annotation, + recording=recording, + ) + assert isinstance(clip_annotation, data.ClipAnnotation) diff --git a/tests/test_io/test_crowsetta/test_segments.py b/tests/test_io/test_crowsetta/test_segments.py index c7d8c4a..0c3f332 100644 --- a/tests/test_io/test_crowsetta/test_segments.py +++ b/tests/test_io/test_crowsetta/test_segments.py @@ -59,6 +59,22 @@ def test_segment_from_annotation_fails_on_empty_geometry( io.crowsetta.segment_from_annotation(sound_event_annotation) +def test_segment_to_annotation_without_onset_and_offset_in_seconds( + recording: data.Recording, +): + samplerate = recording.samplerate + segment = crowsetta.Segment.from_keyword( + label="dog", + onset_sample=3000, + offset_sample=4000, + ) + annotation = io.crowsetta.segment_to_annotation(segment, recording) + assert isinstance(annotation, data.SoundEventAnnotation) + geometry = annotation.sound_event.geometry + assert isinstance(geometry, data.TimeInterval) + assert tuple(geometry.coordinates) == (3000 / samplerate, 4000 / samplerate) + + def test_segment_from_annotation( sound_event_annotation: data.SoundEventAnnotation, ): From 1d3823ec5b2f8d36820ff7f3c46084a656560e31 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 20:13:25 +0000 Subject: [PATCH 03/10] Added compatibility for crowsetta Segment instance creation in version 4 and 5 --- pyproject.toml | 2 +- src/soundevent/io/crowsetta/segment.py | 45 +++++++++++++++++-- .../test_io/test_crowsetta/test_annotation.py | 13 +++--- tests/test_io/test_crowsetta/test_segments.py | 7 ++- tests/test_io/test_crowsetta/test_sequence.py | 15 ++++--- 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ada1f98..ec40850 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ audio = [ ] evaluation = ["scikit-learn>=1.3.2"] geometry = ["shapely>=2.0.1"] -crowsetta = ["crowsetta>=4.0.0.post2"] +crowsetta = ["crowsetta>=4.0.0.post2,<5.0"] all = [ "soundevent[plot]", "soundevent[audio]", diff --git a/src/soundevent/io/crowsetta/segment.py b/src/soundevent/io/crowsetta/segment.py index 531dfe1..259cf3d 100644 --- a/src/soundevent/io/crowsetta/segment.py +++ b/src/soundevent/io/crowsetta/segment.py @@ -2,7 +2,7 @@ from typing import List, Optional -from crowsetta import Segment +import crowsetta from soundevent import data from soundevent.geometry import compute_bounds @@ -36,11 +36,48 @@ def convert_time_to_sample(recording: data.Recording, time: float) -> int: return int(time * recording.samplerate) +crowsetta_version = crowsetta.__version__[0] + +if crowsetta_version == "4": + + def create_crowsetta_segment( + onset_s: Optional[float] = None, + offset_s: Optional[float] = None, + onset_sample: Optional[int] = None, + offset_sample: Optional[int] = None, + label: Optional[str] = None, + ) -> crowsetta.Segment: + return crowsetta.Segment.from_keyword( + label=label, + onset_s=onset_s, + offset_s=offset_s, + onset_sample=onset_sample, + offset_sample=offset_sample, + ) + +else: + + def create_crowsetta_segment( + onset_s: Optional[float] = None, + offset_s: Optional[float] = None, + onset_sample: Optional[int] = None, + offset_sample: Optional[int] = None, + label: Optional[str] = None, + ) -> crowsetta.Segment: + return crowsetta.Segment( + onset_s=onset_s, # type: ignore + offset_s=offset_s, # type: ignore + onset_sample=onset_sample, # type: ignore + offset_sample=offset_sample, # type: ignore + label=label, # type: ignore + ) + + def segment_from_annotation( obj: data.SoundEventAnnotation, cast_to_segment: bool = True, **kwargs, -) -> Segment: +) -> crowsetta.Segment: """Convert a soundevent annotation to a crowsetta segment. This function transforms a SoundEventAnnotation object into a Crowsetta @@ -89,7 +126,7 @@ def segment_from_annotation( end_sample = convert_time_to_sample(sound_event.recording, end_time) label = label_from_tags(obj.tags, **kwargs) - return Segment.from_keyword( + return create_crowsetta_segment( onset_s=start_time, offset_s=end_time, onset_sample=start_sample, @@ -99,7 +136,7 @@ def segment_from_annotation( def segment_to_annotation( - segment: Segment, + segment: crowsetta.Segment, recording: data.Recording, adjust_time_expansion: bool = True, notes: Optional[List[data.Note]] = None, diff --git a/tests/test_io/test_crowsetta/test_annotation.py b/tests/test_io/test_crowsetta/test_annotation.py index df80ac7..a8a856e 100644 --- a/tests/test_io/test_crowsetta/test_annotation.py +++ b/tests/test_io/test_crowsetta/test_annotation.py @@ -7,6 +7,7 @@ import pytest from soundevent import data, io +from soundevent.io.crowsetta.segment import create_crowsetta_segment @pytest.fixture @@ -67,12 +68,12 @@ def sequence_annotation( notated_path=recording.path, seq=crowsetta.Sequence.from_segments( [ - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="dog", onset_s=0.5, offset_s=1.5, ), - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="cat", onset_s=2.0, offset_s=2.5, @@ -240,12 +241,12 @@ def test_annotation_to_clip_annotation_fails_without_path(tmp_path: Path): annot_path=tmp_path / "annotation.txt", seq=crowsetta.Sequence.from_segments( [ - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="dog", onset_s=0.5, offset_s=1.5, ), - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="cat", onset_s=2.0, offset_s=2.5, @@ -267,12 +268,12 @@ def test_annotation_to_clp_annotation_fails_if_paths_dont_match( notated_path=tmp_path / "non_existent.wav", seq=crowsetta.Sequence.from_segments( [ - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="dog", onset_s=0.5, offset_s=1.5, ), - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="cat", onset_s=2.0, offset_s=2.5, diff --git a/tests/test_io/test_crowsetta/test_segments.py b/tests/test_io/test_crowsetta/test_segments.py index 0c3f332..eed4f43 100644 --- a/tests/test_io/test_crowsetta/test_segments.py +++ b/tests/test_io/test_crowsetta/test_segments.py @@ -4,6 +4,9 @@ import pytest from soundevent import data, io +from soundevent.io.crowsetta.segment import ( + create_crowsetta_segment, +) @pytest.fixture @@ -38,7 +41,7 @@ def sound_event_annotation( @pytest.fixture def segment() -> crowsetta.Segment: - return crowsetta.Segment.from_keyword( + return create_crowsetta_segment( label="dog", onset_s=0.5, offset_s=1.5, @@ -63,7 +66,7 @@ def test_segment_to_annotation_without_onset_and_offset_in_seconds( recording: data.Recording, ): samplerate = recording.samplerate - segment = crowsetta.Segment.from_keyword( + segment = create_crowsetta_segment( label="dog", onset_sample=3000, offset_sample=4000, diff --git a/tests/test_io/test_crowsetta/test_sequence.py b/tests/test_io/test_crowsetta/test_sequence.py index 0544aca..fdaa95e 100644 --- a/tests/test_io/test_crowsetta/test_sequence.py +++ b/tests/test_io/test_crowsetta/test_sequence.py @@ -7,6 +7,7 @@ import pytest from soundevent import data, io +from soundevent.io.crowsetta.segment import create_crowsetta_segment @pytest.fixture @@ -55,12 +56,12 @@ def sound_event_annotations( def sequence() -> crowsetta.Sequence: return crowsetta.Sequence.from_segments( [ - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="dog", onset_s=0.5, offset_s=1.5, ), - crowsetta.Segment.from_keyword( + create_crowsetta_segment( label="cat", onset_s=2.0, offset_s=2.5, @@ -92,7 +93,7 @@ def test_sequence_from_annotations_fails_on_non_compatible_geometries( data.SoundEventAnnotation( sound_event=data.SoundEvent( recording=recording, - geometry=data.TimeInterval(coordinates=[0.5, 1.5]) + geometry=data.TimeInterval(coordinates=[0.5, 1.5]), ), ), ] @@ -117,7 +118,7 @@ def test_sequence_from_annotations_ignores_non_compatible_geometries( data.SoundEventAnnotation( sound_event=data.SoundEvent( recording=recording, - geometry=data.TimeInterval(coordinates=[0.5, 1.5]) + geometry=data.TimeInterval(coordinates=[0.5, 1.5]), ), ), ] @@ -144,7 +145,7 @@ def test_sequence_from_annotation_casts_to_geometry( data.SoundEventAnnotation( sound_event=data.SoundEvent( recording=recording, - geometry=data.TimeInterval(coordinates=[0.5, 1.5]) + geometry=data.TimeInterval(coordinates=[0.5, 1.5]), ), ), ] @@ -168,5 +169,7 @@ def test_sequence_to_annotations( recording, ) assert len(annotations) == 2 - assert all(isinstance(ann, data.SoundEventAnnotation) for ann in annotations) + assert all( + isinstance(ann, data.SoundEventAnnotation) for ann in annotations + ) assert all(ann.sound_event.recording == recording for ann in annotations) From 8cd709173aaaf67fcf033bce93e833c3da2c541a Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 20:15:18 +0000 Subject: [PATCH 04/10] Ran formaters --- src/soundevent/__version__.py | 1 + src/soundevent/audio/chunks.py | 1 + src/soundevent/audio/io.py | 1 + src/soundevent/audio/media_info.py | 1 + src/soundevent/audio/spectrum.py | 1 + src/soundevent/data/clip_annotations.py | 1 + src/soundevent/data/clip_evaluations.py | 1 + src/soundevent/data/clip_predictions.py | 1 + src/soundevent/data/evaluations.py | 1 + src/soundevent/data/geometries.py | 1 + src/soundevent/data/model_runs.py | 1 - src/soundevent/data/predicted_tags.py | 1 + src/soundevent/data/prediction_sets.py | 1 + src/soundevent/data/sequence_annotations.py | 1 - src/soundevent/data/sequence_predictions.py | 1 - src/soundevent/data/sequences.py | 1 + .../data/sound_event_predictions.py | 1 + src/soundevent/evaluation/__init__.py | 1 + src/soundevent/evaluation/affinity.py | 1 - .../tasks/clip_multilabel_classification.py | 6 +- .../tasks/sound_event_classification.py | 1 + .../evaluation/tasks/sound_event_detection.py | 1 + src/soundevent/geometry/features.py | 1 + src/soundevent/geometry/html.py | 17 ++++-- src/soundevent/io/aoef/__init__.py | 1 + src/soundevent/io/aoef/adapters.py | 9 +-- src/soundevent/io/aoef/annotation_task.py | 36 +++++++----- src/soundevent/io/aoef/clip.py | 28 ++++++---- src/soundevent/io/aoef/clip_annotations.py | 46 +++++++++------ src/soundevent/io/aoef/clip_evaluation.py | 21 ++++--- src/soundevent/io/aoef/clip_predictions.py | 56 +++++++++++-------- src/soundevent/io/aoef/evaluation.py | 16 +++--- src/soundevent/io/aoef/evaluation_set.py | 13 +++-- src/soundevent/io/aoef/match.py | 8 ++- src/soundevent/io/aoef/recording.py | 20 ++++--- src/soundevent/io/aoef/sequence.py | 9 +-- src/soundevent/io/aoef/sequence_annotation.py | 40 ++++++++----- src/soundevent/io/aoef/sequence_prediction.py | 18 +++--- src/soundevent/io/aoef/sound_event.py | 8 ++- .../io/aoef/sound_event_annotation.py | 32 +++++++---- .../io/aoef/sound_event_prediction.py | 18 +++--- src/soundevent/io/formats.py | 1 + src/soundevent/io/types.py | 1 + src/soundevent/plot/__init__.py | 1 + src/soundevent/plot/common.py | 1 + src/soundevent/plot/geometries.py | 4 +- tests/conftest.py | 1 + tests/test_audio/conftest.py | 1 + tests/test_audio/test_audio.py | 1 + tests/test_audio/test_media_info.py | 1 + .../test_clip_multilabel_classification.py | 1 - tests/test_evaluation/test_encode.py | 1 + .../test_sound_event_detection.py | 1 + tests/test_geometry/test_html.py | 1 + tests/test_geometry/test_operations.py | 1 + tests/test_io/conftest.py | 1 + tests/test_io/test_aoef/test_api.py | 1 + tests/test_io/test_aoef/test_match.py | 1 - tests/test_io/test_aoef/test_model_run.py | 1 - .../test_aoef/test_sound_event_annotation.py | 1 - tests/test_io/test_crowsetta/test_segments.py | 5 +- tests/test_soundevent.py | 1 + 62 files changed, 278 insertions(+), 174 deletions(-) diff --git a/src/soundevent/__version__.py b/src/soundevent/__version__.py index faf36b7..7ef0218 100644 --- a/src/soundevent/__version__.py +++ b/src/soundevent/__version__.py @@ -1,2 +1,3 @@ """Set version number for package.""" + __version__ = "1.3.5" diff --git a/src/soundevent/audio/chunks.py b/src/soundevent/audio/chunks.py index 9486770..3f2b2d5 100644 --- a/src/soundevent/audio/chunks.py +++ b/src/soundevent/audio/chunks.py @@ -12,6 +12,7 @@ format is also used for storing other types of data, such as text, images, and metadata. """ + import os from dataclasses import dataclass, field from typing import BinaryIO, Dict, List, Optional diff --git a/src/soundevent/audio/io.py b/src/soundevent/audio/io.py index 8ad15c4..bbe4624 100644 --- a/src/soundevent/audio/io.py +++ b/src/soundevent/audio/io.py @@ -2,6 +2,7 @@ Currently only supports reading and writing of .wav files. """ + from io import BytesIO from pathlib import Path from typing import Dict, Optional, Tuple diff --git a/src/soundevent/audio/media_info.py b/src/soundevent/audio/media_info.py index 6f43429..e3ac9e6 100644 --- a/src/soundevent/audio/media_info.py +++ b/src/soundevent/audio/media_info.py @@ -1,4 +1,5 @@ """Functions for getting media information from WAV files.""" + import hashlib import struct from dataclasses import dataclass diff --git a/src/soundevent/audio/spectrum.py b/src/soundevent/audio/spectrum.py index bcd9fa2..57a4738 100644 --- a/src/soundevent/audio/spectrum.py +++ b/src/soundevent/audio/spectrum.py @@ -33,6 +33,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ + from typing import Optional import numpy as np diff --git a/src/soundevent/data/clip_annotations.py b/src/soundevent/data/clip_annotations.py index 46da2db..f8d4686 100644 --- a/src/soundevent/data/clip_annotations.py +++ b/src/soundevent/data/clip_annotations.py @@ -5,6 +5,7 @@ the clip, associated tags, annotations, status badges indicating the state of the annotation process, and any additional notes related to the annotations. """ + import datetime from typing import List from uuid import UUID, uuid4 diff --git a/src/soundevent/data/clip_evaluations.py b/src/soundevent/data/clip_evaluations.py index e6b0fc1..ad812b0 100644 --- a/src/soundevent/data/clip_evaluations.py +++ b/src/soundevent/data/clip_evaluations.py @@ -7,6 +7,7 @@ predictions (`ProcessedClip`), matches between predicted and ground truth annotations (`Match` instances), and computed evaluation metrics. """ + from typing import Optional, Sequence from uuid import UUID, uuid4 diff --git a/src/soundevent/data/clip_predictions.py b/src/soundevent/data/clip_predictions.py index 8c0ef32..171ef0b 100644 --- a/src/soundevent/data/clip_predictions.py +++ b/src/soundevent/data/clip_predictions.py @@ -34,6 +34,7 @@ These annotations and additional information provide more detailed insights into the predicted events and aid in subsequent analysis and interpretation. """ + from typing import Sequence from uuid import UUID, uuid4 diff --git a/src/soundevent/data/evaluations.py b/src/soundevent/data/evaluations.py index f8f00ac..d55db26 100644 --- a/src/soundevent/data/evaluations.py +++ b/src/soundevent/data/evaluations.py @@ -6,6 +6,7 @@ evaluation comprises an `EvaluationSet`, a specific `ModelRun`, computed metrics, and a set of `EvaluatedExample` instances. """ + import datetime from typing import Optional, Sequence from uuid import UUID, uuid4 diff --git a/src/soundevent/data/geometries.py b/src/soundevent/data/geometries.py index 773195b..35b8c37 100644 --- a/src/soundevent/data/geometries.py +++ b/src/soundevent/data/geometries.py @@ -51,6 +51,7 @@ comprehensive framework for accurately and flexibly describing the location and extent of sound events within a recording. """ + import json import sys from abc import ABC diff --git a/src/soundevent/data/model_runs.py b/src/soundevent/data/model_runs.py index 6fa8f7e..f774a21 100644 --- a/src/soundevent/data/model_runs.py +++ b/src/soundevent/data/model_runs.py @@ -1,6 +1,5 @@ """Model Run Module.""" - from typing import Optional from soundevent.data.prediction_sets import PredictionSet diff --git a/src/soundevent/data/predicted_tags.py b/src/soundevent/data/predicted_tags.py index 50832ed..c3d6d8d 100644 --- a/src/soundevent/data/predicted_tags.py +++ b/src/soundevent/data/predicted_tags.py @@ -23,6 +23,7 @@ information to assess the reliability and accuracy of the predicted tags, facilitating further analysis and evaluation of the audio data. """ + from pydantic import BaseModel, Field from soundevent.data.tags import Tag diff --git a/src/soundevent/data/prediction_sets.py b/src/soundevent/data/prediction_sets.py index 8e2054c..edffd2a 100644 --- a/src/soundevent/data/prediction_sets.py +++ b/src/soundevent/data/prediction_sets.py @@ -34,6 +34,7 @@ performance of various models, algorithms, or parameter settings by examining the processed clips within each `ModelRun`. """ + import datetime from typing import List from uuid import UUID, uuid4 diff --git a/src/soundevent/data/sequence_annotations.py b/src/soundevent/data/sequence_annotations.py index 2bc5130..47e2dfa 100644 --- a/src/soundevent/data/sequence_annotations.py +++ b/src/soundevent/data/sequence_annotations.py @@ -1,6 +1,5 @@ """Sequence Annotation Class.""" - import datetime from typing import List, Optional from uuid import UUID, uuid4 diff --git a/src/soundevent/data/sequence_predictions.py b/src/soundevent/data/sequence_predictions.py index cde5066..490a46d 100644 --- a/src/soundevent/data/sequence_predictions.py +++ b/src/soundevent/data/sequence_predictions.py @@ -1,6 +1,5 @@ """Sequence Prediction Class.""" - from typing import List from uuid import UUID, uuid4 diff --git a/src/soundevent/data/sequences.py b/src/soundevent/data/sequences.py index 47cace7..211b821 100644 --- a/src/soundevent/data/sequences.py +++ b/src/soundevent/data/sequences.py @@ -37,6 +37,7 @@ features, and hierarchical capabilities, provides a powerful tool for understanding the rich complexity of vocalization sequences. """ + from typing import List, Optional from uuid import UUID, uuid4 diff --git a/src/soundevent/data/sound_event_predictions.py b/src/soundevent/data/sound_event_predictions.py index 4722233..c71afec 100644 --- a/src/soundevent/data/sound_event_predictions.py +++ b/src/soundevent/data/sound_event_predictions.py @@ -1,4 +1,5 @@ """Sound Event Predictions.""" + from typing import List from uuid import UUID, uuid4 diff --git a/src/soundevent/evaluation/__init__.py b/src/soundevent/evaluation/__init__.py index b9f4c1b..273303f 100644 --- a/src/soundevent/evaluation/__init__.py +++ b/src/soundevent/evaluation/__init__.py @@ -1,4 +1,5 @@ """Evaluation functions.""" + from soundevent.evaluation.affinity import compute_affinity from soundevent.evaluation.encoding import ( classification_encoding, diff --git a/src/soundevent/evaluation/affinity.py b/src/soundevent/evaluation/affinity.py index bff2d87..801e836 100644 --- a/src/soundevent/evaluation/affinity.py +++ b/src/soundevent/evaluation/affinity.py @@ -1,6 +1,5 @@ """Measures of affinity between sound events geometries.""" - from soundevent import data from soundevent.geometry import ( buffer_geometry, diff --git a/src/soundevent/evaluation/tasks/clip_multilabel_classification.py b/src/soundevent/evaluation/tasks/clip_multilabel_classification.py index 413a034..c9f23a5 100644 --- a/src/soundevent/evaluation/tasks/clip_multilabel_classification.py +++ b/src/soundevent/evaluation/tasks/clip_multilabel_classification.py @@ -58,7 +58,11 @@ def _evaluate_clips( clip_predictions: Sequence[data.ClipPrediction], clip_annotations: Sequence[data.ClipAnnotation], encoder: Encoder, -) -> Tuple[List[data.ClipEvaluation], np.ndarray, np.ndarray,]: +) -> Tuple[ + List[data.ClipEvaluation], + np.ndarray, + np.ndarray, +]: """Evaluate all examples in the given model run and evaluation set.""" evaluated_clips = [] true_classes = [] diff --git a/src/soundevent/evaluation/tasks/sound_event_classification.py b/src/soundevent/evaluation/tasks/sound_event_classification.py index 1ec6509..c860467 100644 --- a/src/soundevent/evaluation/tasks/sound_event_classification.py +++ b/src/soundevent/evaluation/tasks/sound_event_classification.py @@ -1,4 +1,5 @@ """Sound event classification evaluation.""" + from typing import List, Optional, Sequence, Tuple import numpy as np diff --git a/src/soundevent/evaluation/tasks/sound_event_detection.py b/src/soundevent/evaluation/tasks/sound_event_detection.py index ade64c3..a841e55 100644 --- a/src/soundevent/evaluation/tasks/sound_event_detection.py +++ b/src/soundevent/evaluation/tasks/sound_event_detection.py @@ -1,4 +1,5 @@ """Sound event detection evaluation.""" + from typing import List, Optional, Sequence, Tuple import numpy as np diff --git a/src/soundevent/geometry/features.py b/src/soundevent/geometry/features.py index bdac0b3..a251bfe 100644 --- a/src/soundevent/geometry/features.py +++ b/src/soundevent/geometry/features.py @@ -34,6 +34,7 @@ Feature(name='high_freq', value=1000), Feature(name='bandwidth', value=1000)] """ + from enum import Enum from typing import Any, Callable, Dict, List diff --git a/src/soundevent/geometry/html.py b/src/soundevent/geometry/html.py index 36c9e0f..e705c85 100644 --- a/src/soundevent/geometry/html.py +++ b/src/soundevent/geometry/html.py @@ -1,4 +1,5 @@ """HTML representation of geometries.""" + import shapely from soundevent import data @@ -93,18 +94,22 @@ def axis_label( "position: absolute", "top: 0; " if top else "bottom: 0", "left: 0; " if left else "right: 0", - "transform: translate(-105%, 0)" - if axis == "freq" - else "transform: translate(0, 100%)", + ( + "transform: translate(-105%, 0)" + if axis == "freq" + else "transform: translate(0, 100%)" + ), ] ) inner_style = "; ".join( [ "display: inline", - "vertical-align: top" - if axis == "time" - else "vertical-align: bottom", + ( + "vertical-align: top" + if axis == "time" + else "vertical-align: bottom" + ), ] ) diff --git a/src/soundevent/io/aoef/__init__.py b/src/soundevent/io/aoef/__init__.py index 0d345c1..08659c8 100644 --- a/src/soundevent/io/aoef/__init__.py +++ b/src/soundevent/io/aoef/__init__.py @@ -24,6 +24,7 @@ risk of errors and inconsistencies. """ + import datetime from pathlib import Path from typing import Any, Dict, Optional, Set, TypeVar, Union diff --git a/src/soundevent/io/aoef/adapters.py b/src/soundevent/io/aoef/adapters.py index 353d243..ac21ca3 100644 --- a/src/soundevent/io/aoef/adapters.py +++ b/src/soundevent/io/aoef/adapters.py @@ -40,14 +40,11 @@ class AdapterProtocol(Protocol, Generic[C, D]): def __init__( self, audio_dir: Optional[data.PathLike] = None, - ): - ... + ): ... - def to_aoef(self, obj: C) -> D: - ... + def to_aoef(self, obj: C) -> D: ... - def to_soundevent(self, obj: D) -> C: - ... + def to_soundevent(self, obj: D) -> C: ... class DataAdapter( diff --git a/src/soundevent/io/aoef/annotation_task.py b/src/soundevent/io/aoef/annotation_task.py index f10173d..3079334 100644 --- a/src/soundevent/io/aoef/annotation_task.py +++ b/src/soundevent/io/aoef/annotation_task.py @@ -52,18 +52,22 @@ def assemble_aoef( return AnnotationTaskObject( uuid=obj.uuid, clip=self.clip_adapter.to_aoef(obj.clip).uuid, - status_badges=[ - StatusBadgeObject( - state=badge.state, - owner=self.user_adapter.to_aoef(badge.owner).uuid - if badge.owner is not None - else None, - created_on=badge.created_on, - ) - for badge in obj.status_badges - ] - if obj.status_badges - else None, + status_badges=( + [ + StatusBadgeObject( + state=badge.state, + owner=( + self.user_adapter.to_aoef(badge.owner).uuid + if badge.owner is not None + else None + ), + created_on=badge.created_on, + ) + for badge in obj.status_badges + ] + if obj.status_badges + else None + ), created_on=obj.created_on, ) @@ -82,9 +86,11 @@ def assemble_soundevent( status_badges=[ data.StatusBadge( state=badge.state, - owner=self.user_adapter.from_id(badge.owner) - if badge.owner is not None - else None, + owner=( + self.user_adapter.from_id(badge.owner) + if badge.owner is not None + else None + ), created_on=badge.created_on or datetime.datetime.now(), ) for badge in obj.status_badges or [] diff --git a/src/soundevent/io/aoef/clip.py b/src/soundevent/io/aoef/clip.py index b8cd8d1..14b0ff7 100644 --- a/src/soundevent/io/aoef/clip.py +++ b/src/soundevent/io/aoef/clip.py @@ -33,9 +33,11 @@ def assemble_aoef(self, obj: data.Clip, obj_id: UUID) -> ClipObject: start_time=obj.start_time, end_time=obj.end_time, uuid=obj.uuid, - features={feature.name: feature.value for feature in obj.features} - if obj.features - else None, + features=( + {feature.name: feature.value for feature in obj.features} + if obj.features + else None + ), ) def assemble_soundevent( @@ -52,13 +54,15 @@ def assemble_soundevent( start_time=obj.start_time, end_time=obj.end_time, uuid=obj.uuid or uuid4(), - features=[ - data.Feature( - name=name, - value=value, - ) - for name, value in obj.features.items() - ] - if obj.features - else [], + features=( + [ + data.Feature( + name=name, + value=value, + ) + for name, value in obj.features.items() + ] + if obj.features + else [] + ), ) diff --git a/src/soundevent/io/aoef/clip_annotations.py b/src/soundevent/io/aoef/clip_annotations.py index bae7be0..c74cbe6 100644 --- a/src/soundevent/io/aoef/clip_annotations.py +++ b/src/soundevent/io/aoef/clip_annotations.py @@ -52,24 +52,34 @@ def assemble_aoef( return ClipAnnotationsObject( uuid=obj.uuid, clip=self.clip_adapter.to_aoef(obj.clip).uuid, - tags=[self.tag_adapter.to_aoef(tag).id for tag in obj.tags] - if obj.tags - else None, - sound_events=[ - self.sound_event_annotation_adapter.to_aoef(annotation).uuid - for annotation in obj.sound_events - ] - if obj.sound_events - else None, - sequences=[ - self.sequence_annotation_adapter.to_aoef(annotation).uuid - for annotation in obj.sequences - ] - if obj.sequences - else None, - notes=[self.note_adapter.to_aoef(note) for note in obj.notes] - if obj.notes - else None, + tags=( + [self.tag_adapter.to_aoef(tag).id for tag in obj.tags] + if obj.tags + else None + ), + sound_events=( + [ + self.sound_event_annotation_adapter.to_aoef( + annotation + ).uuid + for annotation in obj.sound_events + ] + if obj.sound_events + else None + ), + sequences=( + [ + self.sequence_annotation_adapter.to_aoef(annotation).uuid + for annotation in obj.sequences + ] + if obj.sequences + else None + ), + notes=( + [self.note_adapter.to_aoef(note) for note in obj.notes] + if obj.notes + else None + ), created_on=obj.created_on, ) diff --git a/src/soundevent/io/aoef/clip_evaluation.py b/src/soundevent/io/aoef/clip_evaluation.py index 1ab7108..0cddee6 100644 --- a/src/soundevent/io/aoef/clip_evaluation.py +++ b/src/soundevent/io/aoef/clip_evaluation.py @@ -49,14 +49,19 @@ def assemble_aoef( uuid=obj.uuid, annotations=annotations.uuid, predictions=predictions.uuid, - matches=[ - self.match_adapter.to_aoef(match).uuid for match in obj.matches - ] - if obj.matches - else None, - metrics={metrics.name: metrics.value for metrics in obj.metrics} - if obj.metrics - else None, + matches=( + [ + self.match_adapter.to_aoef(match).uuid + for match in obj.matches + ] + if obj.matches + else None + ), + metrics=( + {metrics.name: metrics.value for metrics in obj.metrics} + if obj.metrics + else None + ), score=obj.score, ) diff --git a/src/soundevent/io/aoef/clip_predictions.py b/src/soundevent/io/aoef/clip_predictions.py index 2aa0733..df7dc2c 100644 --- a/src/soundevent/io/aoef/clip_predictions.py +++ b/src/soundevent/io/aoef/clip_predictions.py @@ -45,29 +45,39 @@ def assemble_aoef( return ClipPredictionsObject( uuid=obj.uuid, clip=self.clip_adapter.to_aoef(obj.clip).uuid, - sound_events=[ - self.sound_event_prediction_adapter.to_aoef(sound_event).uuid - for sound_event in obj.sound_events - ] - if obj.sound_events - else None, - sequences=[ - self.sequence_prediction_adapter.to_aoef(sequence).uuid - for sequence in obj.sequences - ] - if obj.sequences - else None, - tags=[ - (tag.id, predicted_tag.score) - for predicted_tag in obj.tags - if (tag := self.tag_adapter.to_aoef(predicted_tag.tag)) - is not None - ] - if obj.tags - else None, - features={feature.name: feature.value for feature in obj.features} - if obj.features is not None - else None, + sound_events=( + [ + self.sound_event_prediction_adapter.to_aoef( + sound_event + ).uuid + for sound_event in obj.sound_events + ] + if obj.sound_events + else None + ), + sequences=( + [ + self.sequence_prediction_adapter.to_aoef(sequence).uuid + for sequence in obj.sequences + ] + if obj.sequences + else None + ), + tags=( + [ + (tag.id, predicted_tag.score) + for predicted_tag in obj.tags + if (tag := self.tag_adapter.to_aoef(predicted_tag.tag)) + is not None + ] + if obj.tags + else None + ), + features=( + {feature.name: feature.value for feature in obj.features} + if obj.features is not None + else None + ), ) def assemble_soundevent( diff --git a/src/soundevent/io/aoef/evaluation.py b/src/soundevent/io/aoef/evaluation.py index 9aa7942..e53e41f 100644 --- a/src/soundevent/io/aoef/evaluation.py +++ b/src/soundevent/io/aoef/evaluation.py @@ -189,13 +189,15 @@ def to_aoef(self, obj: data.Evaluation) -> EvaluationObject: sequence_predictions=self.sequence_prediction_adapter.values(), clip_predictions=self.clip_predictions_adapter.values(), clip_evaluations=self.clip_evaluation_adapter.values(), - metrics={ - metric.name: metric.value - for metric in obj.metrics - if metric.value is not None - } - if obj.metrics - else None, + metrics=( + { + metric.name: metric.value + for metric in obj.metrics + if metric.value is not None + } + if obj.metrics + else None + ), score=obj.score, matches=self.match_adapter.values(), ) diff --git a/src/soundevent/io/aoef/evaluation_set.py b/src/soundevent/io/aoef/evaluation_set.py index ead17dd..53b9e9f 100644 --- a/src/soundevent/io/aoef/evaluation_set.py +++ b/src/soundevent/io/aoef/evaluation_set.py @@ -34,11 +34,14 @@ def to_aoef( # type: ignore created_on=obj.created_on, name=obj.name, description=obj.description, - evaluation_tags=[ - self.tag_adapter.to_aoef(tag).id for tag in obj.evaluation_tags - ] - if obj.evaluation_tags - else None, + evaluation_tags=( + [ + self.tag_adapter.to_aoef(tag).id + for tag in obj.evaluation_tags + ] + if obj.evaluation_tags + else None + ), ) def to_soundevent( # type: ignore diff --git a/src/soundevent/io/aoef/match.py b/src/soundevent/io/aoef/match.py index cd6ab8d..fcc2a9a 100644 --- a/src/soundevent/io/aoef/match.py +++ b/src/soundevent/io/aoef/match.py @@ -54,9 +54,11 @@ def assemble_aoef( target=target, affinity=obj.affinity, score=obj.score, - metrics={metrics.name: metrics.value for metrics in obj.metrics} - if obj.metrics - else None, + metrics=( + {metrics.name: metrics.value for metrics in obj.metrics} + if obj.metrics + else None + ), ) def assemble_soundevent( diff --git a/src/soundevent/io/aoef/recording.py b/src/soundevent/io/aoef/recording.py index 3da9fca..59e9021 100644 --- a/src/soundevent/io/aoef/recording.py +++ b/src/soundevent/io/aoef/recording.py @@ -74,18 +74,20 @@ def assemble_aoef( duration=obj.duration, channels=obj.channels, samplerate=obj.samplerate, - time_expansion=obj.time_expansion - if obj.time_expansion != 1.0 - else None, + time_expansion=( + obj.time_expansion if obj.time_expansion != 1.0 else None + ), hash=obj.hash, date=obj.date, time=obj.time, latitude=obj.latitude, longitude=obj.longitude, tags=tag_ids if tag_ids else None, - features={feature.name: feature.value for feature in obj.features} - if obj.features - else None, + features=( + {feature.name: feature.value for feature in obj.features} + if obj.features + else None + ), notes=notes if notes else None, owners=owners, rights=obj.rights, @@ -119,9 +121,9 @@ def assemble_soundevent(self, obj: RecordingObject) -> data.Recording: duration=obj.duration, channels=obj.channels, samplerate=obj.samplerate, - time_expansion=obj.time_expansion - if obj.time_expansion is not None - else 1.0, + time_expansion=( + obj.time_expansion if obj.time_expansion is not None else 1.0 + ), hash=obj.hash, date=obj.date, time=obj.time, diff --git a/src/soundevent/io/aoef/sequence.py b/src/soundevent/io/aoef/sequence.py index 457b6d8..2c1e53e 100644 --- a/src/soundevent/io/aoef/sequence.py +++ b/src/soundevent/io/aoef/sequence.py @@ -1,6 +1,5 @@ """Sequence object in AOEF format.""" - from typing import Dict, List, Optional from uuid import UUID, uuid4 @@ -45,9 +44,11 @@ def assemble_aoef( uuid=obj.uuid, sound_events=sound_events, parent=parent, - features={feature.name: feature.value for feature in obj.features} - if obj.features - else None, + features=( + {feature.name: feature.value for feature in obj.features} + if obj.features + else None + ), ) def assemble_soundevent( diff --git a/src/soundevent/io/aoef/sequence_annotation.py b/src/soundevent/io/aoef/sequence_annotation.py index 2d61a69..1a32085 100644 --- a/src/soundevent/io/aoef/sequence_annotation.py +++ b/src/soundevent/io/aoef/sequence_annotation.py @@ -47,16 +47,22 @@ def assemble_aoef( ) -> SequenceAnnotationObject: return SequenceAnnotationObject( sequence=self.sequence_adapter.to_aoef(obj.sequence).uuid, - notes=[self.note_adapter.to_aoef(note) for note in obj.notes] - if obj.notes - else None, - tags=[self.tag_adapter.to_aoef(tag).id for tag in obj.tags] - if obj.tags - else None, + notes=( + [self.note_adapter.to_aoef(note) for note in obj.notes] + if obj.notes + else None + ), + tags=( + [self.tag_adapter.to_aoef(tag).id for tag in obj.tags] + if obj.tags + else None + ), uuid=obj.uuid, - created_by=self.user_adapter.to_aoef(obj.created_by).uuid - if obj.created_by - else None, + created_by=( + self.user_adapter.to_aoef(obj.created_by).uuid + if obj.created_by + else None + ), created_on=obj.created_on, ) @@ -72,16 +78,20 @@ def assemble_soundevent( return data.SequenceAnnotation( uuid=obj.uuid or uuid4(), sequence=sequence, - notes=[self.note_adapter.to_soundevent(note) for note in obj.notes] - if obj.notes - else [], + notes=( + [self.note_adapter.to_soundevent(note) for note in obj.notes] + if obj.notes + else [] + ), tags=[ tag for tag_id in obj.tags or [] if (tag := self.tag_adapter.from_id(tag_id)) is not None ], - created_by=self.user_adapter.from_id(obj.created_by) - if obj.created_by is not None - else None, + created_by=( + self.user_adapter.from_id(obj.created_by) + if obj.created_by is not None + else None + ), created_on=obj.created_on or datetime.datetime.now(), ) diff --git a/src/soundevent/io/aoef/sequence_prediction.py b/src/soundevent/io/aoef/sequence_prediction.py index 818dd50..636f3df 100644 --- a/src/soundevent/io/aoef/sequence_prediction.py +++ b/src/soundevent/io/aoef/sequence_prediction.py @@ -38,14 +38,16 @@ def assemble_aoef( sequence=self.sequence_adapter.to_aoef(obj.sequence).uuid, uuid=obj.uuid, score=obj.score, - tags=[ - (tag.id, predicted_tag.score) - for predicted_tag in obj.tags - if (tag := self.tag_adapter.to_aoef(predicted_tag.tag)) - is not None - ] - if obj.tags - else None, + tags=( + [ + (tag.id, predicted_tag.score) + for predicted_tag in obj.tags + if (tag := self.tag_adapter.to_aoef(predicted_tag.tag)) + is not None + ] + if obj.tags + else None + ), ) def assemble_soundevent( diff --git a/src/soundevent/io/aoef/sound_event.py b/src/soundevent/io/aoef/sound_event.py index 0448313..9e5a175 100644 --- a/src/soundevent/io/aoef/sound_event.py +++ b/src/soundevent/io/aoef/sound_event.py @@ -37,9 +37,11 @@ def assemble_aoef( geometry=obj.geometry, uuid=obj.uuid, recording=self.recording_adapter.to_aoef(obj.recording).uuid, - features={feature.name: feature.value for feature in obj.features} - if obj.features - else None, + features=( + {feature.name: feature.value for feature in obj.features} + if obj.features + else None + ), ) def assemble_soundevent( diff --git a/src/soundevent/io/aoef/sound_event_annotation.py b/src/soundevent/io/aoef/sound_event_annotation.py index c1f5518..afa07c3 100644 --- a/src/soundevent/io/aoef/sound_event_annotation.py +++ b/src/soundevent/io/aoef/sound_event_annotation.py @@ -46,14 +46,18 @@ def assemble_aoef( ) -> SoundEventAnnotationObject: return SoundEventAnnotationObject( sound_event=self.sound_event_adapter.to_aoef(obj.sound_event).uuid, - notes=[self.note_adapter.to_aoef(note) for note in obj.notes] - if obj.notes - else None, + notes=( + [self.note_adapter.to_aoef(note) for note in obj.notes] + if obj.notes + else None + ), tags=[self.tag_adapter.to_aoef(tag).id for tag in obj.tags], uuid=obj.uuid, - created_by=self.user_adapter.to_aoef(obj.created_by).uuid - if obj.created_by - else None, + created_by=( + self.user_adapter.to_aoef(obj.created_by).uuid + if obj.created_by + else None + ), created_on=obj.created_on, ) @@ -71,16 +75,20 @@ def assemble_soundevent( return data.SoundEventAnnotation( uuid=obj.uuid, sound_event=sound_event, - notes=[self.note_adapter.to_soundevent(note) for note in obj.notes] - if obj.notes - else [], + notes=( + [self.note_adapter.to_soundevent(note) for note in obj.notes] + if obj.notes + else [] + ), tags=[ tag for tag_id in obj.tags or [] if (tag := self.tag_adapter.from_id(tag_id)) is not None ], - created_by=self.user_adapter.from_id(obj.created_by) - if obj.created_by is not None - else None, + created_by=( + self.user_adapter.from_id(obj.created_by) + if obj.created_by is not None + else None + ), created_on=obj.created_on or datetime.datetime.now(), ) diff --git a/src/soundevent/io/aoef/sound_event_prediction.py b/src/soundevent/io/aoef/sound_event_prediction.py index 5e52e5f..175e06a 100644 --- a/src/soundevent/io/aoef/sound_event_prediction.py +++ b/src/soundevent/io/aoef/sound_event_prediction.py @@ -40,14 +40,16 @@ def assemble_aoef( sound_event=self.sound_event_adapter.to_aoef(obj.sound_event).uuid, uuid=obj.uuid, score=obj.score, - tags=[ - (tag.id, predicted_tag.score) - for predicted_tag in obj.tags - if (tag := self.tag_adapter.to_aoef(predicted_tag.tag)) - is not None - ] - if obj.tags - else None, + tags=( + [ + (tag.id, predicted_tag.score) + for predicted_tag in obj.tags + if (tag := self.tag_adapter.to_aoef(predicted_tag.tag)) + is not None + ] + if obj.tags + else None + ), ) def assemble_soundevent( diff --git a/src/soundevent/io/formats.py b/src/soundevent/io/formats.py index af288a1..8f90b14 100644 --- a/src/soundevent/io/formats.py +++ b/src/soundevent/io/formats.py @@ -1,4 +1,5 @@ """Storage formats for soundevent objects.""" + from typing import Callable, Dict from soundevent.data import PathLike diff --git a/src/soundevent/io/types.py b/src/soundevent/io/types.py index 9a9ed7d..2501865 100644 --- a/src/soundevent/io/types.py +++ b/src/soundevent/io/types.py @@ -1,4 +1,5 @@ """Submodule of io module containing type definitions.""" + import sys from typing import Generic, Optional, TypeVar, Union diff --git a/src/soundevent/plot/__init__.py b/src/soundevent/plot/__init__.py index 132cf76..1e79170 100644 --- a/src/soundevent/plot/__init__.py +++ b/src/soundevent/plot/__init__.py @@ -1,4 +1,5 @@ """Plotting utilities.""" + from soundevent.plot.annotation import plot_annotation from soundevent.plot.geometries import plot_geometry from soundevent.plot.spectrogram import plot_spectrogram diff --git a/src/soundevent/plot/common.py b/src/soundevent/plot/common.py index 71d9adb..41216d2 100644 --- a/src/soundevent/plot/common.py +++ b/src/soundevent/plot/common.py @@ -1,4 +1,5 @@ """Common utilities for plotting.""" + from typing import Optional, Tuple import matplotlib.pyplot as plt diff --git a/src/soundevent/plot/geometries.py b/src/soundevent/plot/geometries.py index 1c6304d..f595ff2 100644 --- a/src/soundevent/plot/geometries.py +++ b/src/soundevent/plot/geometries.py @@ -1,4 +1,5 @@ """Functions for plotting sound event geometries.""" + import sys from typing import Dict, Optional @@ -25,8 +26,7 @@ def __call__( geometry: data.Geometry, ax: Axes, **kwargs, - ) -> Axes: - ... + ) -> Axes: ... def plot_geometry( diff --git a/tests/conftest.py b/tests/conftest.py index 3d7b368..4972ace 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """Pytest fixtures for the audio tests.""" + import random import string from pathlib import Path diff --git a/tests/test_audio/conftest.py b/tests/test_audio/conftest.py index 930514c..ffd1cac 100644 --- a/tests/test_audio/conftest.py +++ b/tests/test_audio/conftest.py @@ -1,4 +1,5 @@ """Common fixtures for audio tests.""" + from pathlib import Path import pytest diff --git a/tests/test_audio/test_audio.py b/tests/test_audio/test_audio.py index 4db3f63..dff3c96 100644 --- a/tests/test_audio/test_audio.py +++ b/tests/test_audio/test_audio.py @@ -1,4 +1,5 @@ """Test suite for audio loading functions.""" + from pathlib import Path from uuid import uuid4 diff --git a/tests/test_audio/test_media_info.py b/tests/test_audio/test_media_info.py index cfb8bdc..3bd199b 100644 --- a/tests/test_audio/test_media_info.py +++ b/tests/test_audio/test_media_info.py @@ -1,4 +1,5 @@ """Test suite for soundevent.audio.media_info module.""" + from pathlib import Path from soundevent.audio.media_info import ( diff --git a/tests/test_evaluation/test_clip_multilabel_classification.py b/tests/test_evaluation/test_clip_multilabel_classification.py index e0acb0c..6b74fcb 100644 --- a/tests/test_evaluation/test_clip_multilabel_classification.py +++ b/tests/test_evaluation/test_clip_multilabel_classification.py @@ -1,6 +1,5 @@ """Test suite of the Clip Multi-Label Classification module.""" - from typing import List import pytest diff --git a/tests/test_evaluation/test_encode.py b/tests/test_evaluation/test_encode.py index 55b0acc..cd2758b 100644 --- a/tests/test_evaluation/test_encode.py +++ b/tests/test_evaluation/test_encode.py @@ -1,4 +1,5 @@ """Test suite for the soundevent.evaluation.ecoding module.""" + from typing import Callable, Sequence import numpy as np diff --git a/tests/test_evaluation/test_sound_event_detection.py b/tests/test_evaluation/test_sound_event_detection.py index b550ca1..09080e5 100644 --- a/tests/test_evaluation/test_sound_event_detection.py +++ b/tests/test_evaluation/test_sound_event_detection.py @@ -1,4 +1,5 @@ """Test suite for sound event detection evaluation.""" + from pathlib import Path from soundevent import data, io diff --git a/tests/test_geometry/test_html.py b/tests/test_geometry/test_html.py index ae3a5f9..dd019cf 100644 --- a/tests/test_geometry/test_html.py +++ b/tests/test_geometry/test_html.py @@ -1,4 +1,5 @@ """Test that geometries get converted to HTML.""" + import html5lib from soundevent import data diff --git a/tests/test_geometry/test_operations.py b/tests/test_geometry/test_operations.py index 3935e5a..e400214 100644 --- a/tests/test_geometry/test_operations.py +++ b/tests/test_geometry/test_operations.py @@ -1,4 +1,5 @@ """Test Suite for geometric operations.""" + import math from typing import List diff --git a/tests/test_io/conftest.py b/tests/test_io/conftest.py index b4b445c..725e532 100644 --- a/tests/test_io/conftest.py +++ b/tests/test_io/conftest.py @@ -40,6 +40,7 @@ def recording( ], ) + @pytest.fixture def time_expanded_recording( random_wav: Callable[[], Path], diff --git a/tests/test_io/test_aoef/test_api.py b/tests/test_io/test_aoef/test_api.py index fbc72e8..a278e59 100644 --- a/tests/test_io/test_aoef/test_api.py +++ b/tests/test_io/test_aoef/test_api.py @@ -1,4 +1,5 @@ """Test the basic API of the io functions.""" + import datetime import json from pathlib import Path diff --git a/tests/test_io/test_aoef/test_match.py b/tests/test_io/test_aoef/test_match.py index c7398d0..cebd2f9 100644 --- a/tests/test_io/test_aoef/test_match.py +++ b/tests/test_io/test_aoef/test_match.py @@ -1,6 +1,5 @@ """Test suite for AOEF Match Adapter.""" - from soundevent import data from soundevent.io.aoef.match import MatchAdapter, MatchObject diff --git a/tests/test_io/test_aoef/test_model_run.py b/tests/test_io/test_aoef/test_model_run.py index 296a27b..ddd7cf2 100644 --- a/tests/test_io/test_aoef/test_model_run.py +++ b/tests/test_io/test_aoef/test_model_run.py @@ -1,6 +1,5 @@ """Test suite for AOEF Model Run Adapter.""" - from soundevent import data from soundevent.io.aoef.model_run import ModelRunAdapter, ModelRunObject diff --git a/tests/test_io/test_aoef/test_sound_event_annotation.py b/tests/test_io/test_aoef/test_sound_event_annotation.py index 790908f..82f948b 100644 --- a/tests/test_io/test_aoef/test_sound_event_annotation.py +++ b/tests/test_io/test_aoef/test_sound_event_annotation.py @@ -1,6 +1,5 @@ """Test suite for AOEF sound event annotation adapter.""" - from soundevent import data from soundevent.io.aoef.sound_event_annotation import ( SoundEventAnnotationAdapter, diff --git a/tests/test_io/test_crowsetta/test_segments.py b/tests/test_io/test_crowsetta/test_segments.py index eed4f43..81fd744 100644 --- a/tests/test_io/test_crowsetta/test_segments.py +++ b/tests/test_io/test_crowsetta/test_segments.py @@ -75,7 +75,10 @@ def test_segment_to_annotation_without_onset_and_offset_in_seconds( assert isinstance(annotation, data.SoundEventAnnotation) geometry = annotation.sound_event.geometry assert isinstance(geometry, data.TimeInterval) - assert tuple(geometry.coordinates) == (3000 / samplerate, 4000 / samplerate) + assert tuple(geometry.coordinates) == ( + 3000 / samplerate, + 4000 / samplerate, + ) def test_segment_from_annotation( diff --git a/tests/test_soundevent.py b/tests/test_soundevent.py index 8fe9270..46a4e17 100644 --- a/tests/test_soundevent.py +++ b/tests/test_soundevent.py @@ -1,4 +1,5 @@ """Unit tests for soundevent.""" + import soundevent From 97b27293cf3c577027c873d699287f18a48f43c7 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 20:20:59 +0000 Subject: [PATCH 05/10] Fixed typing issue --- src/soundevent/io/crowsetta/labels.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/soundevent/io/crowsetta/labels.py b/src/soundevent/io/crowsetta/labels.py index 456a630..832d4fb 100644 --- a/src/soundevent/io/crowsetta/labels.py +++ b/src/soundevent/io/crowsetta/labels.py @@ -94,9 +94,8 @@ def label_to_tags( pass if tag_mapping is not None: - tags = tag_mapping.get(label) - - if tags is not None: + if label in tag_mapping: + tags = tag_mapping[label] return tags if isinstance(tags, list) else [tags] if key_mapping is not None: From fa9ce6386c416d240474bb85bef62e1f1fefcb77 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 20:25:02 +0000 Subject: [PATCH 06/10] Skip crowsetta import test on python 3.8 --- tests/test_io/test_crowsetta/test_import.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_io/test_crowsetta/test_import.py b/tests/test_io/test_crowsetta/test_import.py index 555f327..c171297 100644 --- a/tests/test_io/test_crowsetta/test_import.py +++ b/tests/test_io/test_crowsetta/test_import.py @@ -1,5 +1,8 @@ +import sys import warnings +import pytest + warnings.filterwarnings("ignore", category=UserWarning, module="crowsetta") from pathlib import Path @@ -8,6 +11,9 @@ from soundevent import data, io +@pytest.mark.skipif( + sys.version_info < (3, 9), reason="requires python3.9 or higher" +) def test_can_import_all_example_formats( tmp_path: Path, recording: data.Recording, From f6d8c99de93b22c2d70ea681a54fddf4875a7ac2 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 20:31:36 +0000 Subject: [PATCH 07/10] Added importlib-resources as a dependency to crowsetta group --- pdm.lock | 2 +- pyproject.toml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pdm.lock b/pdm.lock index 21299ca..05ff9be 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "all", "audio", "dev", "evaluation", "geometry", "plot", "crowsetta"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:1f8d4f2e93a0dce6ce636f709ee0b787a47611b8be7f7a81de3ab27f397f028a" +content_hash = "sha256:4fa7a884a1bf159b291106f3eb8f890a89dfce0b2dc4888ddefc8542e4444ce2" [[package]] name = "annotated-types" diff --git a/pyproject.toml b/pyproject.toml index ec40850..3769976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,10 @@ audio = [ ] evaluation = ["scikit-learn>=1.3.2"] geometry = ["shapely>=2.0.1"] -crowsetta = ["crowsetta>=4.0.0.post2,<5.0"] +crowsetta = [ + "crowsetta>=4.0.0.post2,<5.0", + "importlib-resources>=6.1.1; python_version < \"3.9\"", +] all = [ "soundevent[plot]", "soundevent[audio]", From 1a6250d17a40528f6750a7d990ade0b22e14ca9b Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 20:39:29 +0000 Subject: [PATCH 08/10] Pin importlib_resources to version >= 5 and < 6 --- pdm.lock | 8 ++++---- pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pdm.lock b/pdm.lock index 05ff9be..5b259cc 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "all", "audio", "dev", "evaluation", "geometry", "plot", "crowsetta"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:4fa7a884a1bf159b291106f3eb8f890a89dfce0b2dc4888ddefc8542e4444ce2" +content_hash = "sha256:fa52cc2cfb9e3e20eea61b9d1d31d9bf9ef990ea321a91ea6e697e7ce7ed58a1" [[package]] name = "annotated-types" @@ -770,15 +770,15 @@ files = [ [[package]] name = "importlib-resources" -version = "6.1.1" +version = "5.13.0" requires_python = ">=3.8" summary = "Read resources from Python packages" dependencies = [ "zipp>=3.1.0; python_version < \"3.10\"", ] files = [ - {file = "importlib_resources-6.1.1-py3-none-any.whl", hash = "sha256:e8bf90d8213b486f428c9c39714b920041cb02c184686a3dee24905aaa8105d6"}, - {file = "importlib_resources-6.1.1.tar.gz", hash = "sha256:3893a00122eafde6894c59914446a512f728a0c1a45f9bb9b63721b6bacf0b4a"}, + {file = "importlib_resources-5.13.0-py3-none-any.whl", hash = "sha256:9f7bd0c97b79972a6cce36a366356d16d5e13b09679c11a58f1014bfdf8e64b2"}, + {file = "importlib_resources-5.13.0.tar.gz", hash = "sha256:82d5c6cca930697dbbd86c93333bb2c2e72861d4789a11c2662b933e5ad2b528"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 3769976..0f77806 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ evaluation = ["scikit-learn>=1.3.2"] geometry = ["shapely>=2.0.1"] crowsetta = [ "crowsetta>=4.0.0.post2,<5.0", - "importlib-resources>=6.1.1; python_version < \"3.9\"", + "importlib-resources<6.0,>=5.0; python_version < \"3.9\"", ] all = [ "soundevent[plot]", From 37c0ba5e88c135b985721c097f7679d6bc5c5955 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Mon, 5 Feb 2024 21:10:08 +0000 Subject: [PATCH 09/10] Avoid crowsetta tests on python 3.8 --- src/soundevent/io/__init__.py | 2 - tests/conftest.py | 5 ++ .../test_io/test_crowsetta/test_annotation.py | 27 ++++---- tests/test_io/test_crowsetta/test_bbox.py | 23 +++---- tests/test_io/test_crowsetta/test_import.py | 8 ++- tests/test_io/test_crowsetta/test_labels.py | 65 ++++++++++--------- tests/test_io/test_crowsetta/test_segments.py | 21 +++--- tests/test_io/test_crowsetta/test_sequence.py | 13 ++-- tests/test_soundevent.py | 4 ++ 9 files changed, 91 insertions(+), 77 deletions(-) diff --git a/src/soundevent/io/__init__.py b/src/soundevent/io/__init__.py index cce61a2..a8f8cf0 100644 --- a/src/soundevent/io/__init__.py +++ b/src/soundevent/io/__init__.py @@ -4,7 +4,6 @@ sound event data. """ -import soundevent.io.crowsetta as crowsetta from soundevent.io.loader import load from soundevent.io.saver import save from soundevent.io.types import DataCollections @@ -13,5 +12,4 @@ "save", "load", "DataCollections", - "crowsetta", ] diff --git a/tests/conftest.py b/tests/conftest.py index 4972ace..1440eca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import random import string +import sys from pathlib import Path from typing import Optional from uuid import uuid4 @@ -12,6 +13,10 @@ from soundevent import data +if sys.version_info < (3, 9): + # NOTE: Crowsetta is not supported in Python 3.8 + collect_ignore_glob = ["test_io/test_crowsetta/*.py"] # pragma: no cover + def get_random_string(length): """Generate a random string of fixed length.""" diff --git a/tests/test_io/test_crowsetta/test_annotation.py b/tests/test_io/test_crowsetta/test_annotation.py index a8a856e..3c90a29 100644 --- a/tests/test_io/test_crowsetta/test_annotation.py +++ b/tests/test_io/test_crowsetta/test_annotation.py @@ -6,7 +6,8 @@ import crowsetta import pytest -from soundevent import data, io +import soundevent.io.crowsetta as crowsetta_io +from soundevent import data from soundevent.io.crowsetta.segment import create_crowsetta_segment @@ -114,7 +115,7 @@ def test_seq_annotation_from_clip_annotation( clip_annotation: data.ClipAnnotation, tmp_path: Path, ): - annotation = io.crowsetta.annotation_from_clip_annotation( + annotation = crowsetta_io.annotation_from_clip_annotation( clip_annotation, annot_path=tmp_path / "annotation.txt", annotation_fmt="seq", @@ -128,7 +129,7 @@ def test_bbox_annotation_from_clip_annotation( clip_annotation: data.ClipAnnotation, tmp_path: Path, ): - annotation = io.crowsetta.annotation_from_clip_annotation( + annotation = crowsetta_io.annotation_from_clip_annotation( clip_annotation, annot_path=tmp_path / "annotation.txt", annotation_fmt="bbox", @@ -146,7 +147,7 @@ def test_annotation_from_clip_annotation_fails_with_unknown_fmt( tmp_path: Path, ): with pytest.raises(ValueError): - io.crowsetta.annotation_from_clip_annotation( + crowsetta_io.annotation_from_clip_annotation( clip_annotation, annot_path=tmp_path / "annotation.txt", annotation_fmt="unknown", # type: ignore @@ -156,7 +157,7 @@ def test_annotation_from_clip_annotation_fails_with_unknown_fmt( def test_annotation_to_clip_annotation_from_seq( sequence_annotation: crowsetta.Annotation, ): - annotation = io.crowsetta.annotation_to_clip_annotation( + annotation = crowsetta_io.annotation_to_clip_annotation( sequence_annotation, ) assert isinstance(annotation, data.ClipAnnotation) @@ -167,7 +168,7 @@ def test_annotation_to_clip_annotation_from_seq( def test_annotation_to_clip_annotation_from_bbox( bbox_annotation: crowsetta.Annotation, ): - annotation = io.crowsetta.annotation_to_clip_annotation( + annotation = crowsetta_io.annotation_to_clip_annotation( bbox_annotation, ) assert isinstance(annotation, data.ClipAnnotation) @@ -182,7 +183,7 @@ def test_annotation_to_clip_annotation_with_notes_and_tags( tags = [ data.Tag(key="soundscape", value="garden"), ] - annotation = io.crowsetta.annotation_to_clip_annotation( + annotation = crowsetta_io.annotation_to_clip_annotation( sequence_annotation, tags=tags, notes=[data.Note(message="random note")], @@ -205,7 +206,7 @@ def test_annotation_to_clip_annotation_with_recording( time=datetime.time(12, 0, 0), ) ) - annotation = io.crowsetta.annotation_to_clip_annotation( + annotation = crowsetta_io.annotation_to_clip_annotation( sequence_annotation, recording=recording, ) @@ -218,7 +219,7 @@ def test_annotation_to_clip_annotation_with_recording( def test_annotation_to_clip_annotation_with_recording_kwargs( sequence_annotation: crowsetta.Annotation, ): - annotation = io.crowsetta.annotation_to_clip_annotation( + annotation = crowsetta_io.annotation_to_clip_annotation( sequence_annotation, recording_kwargs=dict( latitude=10, @@ -256,7 +257,7 @@ def test_annotation_to_clip_annotation_fails_without_path(tmp_path: Path): ) with pytest.raises(ValueError): - io.crowsetta.annotation_to_clip_annotation(annotation) + crowsetta_io.annotation_to_clip_annotation(annotation) def test_annotation_to_clp_annotation_fails_if_paths_dont_match( @@ -284,7 +285,7 @@ def test_annotation_to_clp_annotation_fails_if_paths_dont_match( assert recording.path != annotation.notated_path with pytest.raises(ValueError): - io.crowsetta.annotation_to_clip_annotation( + crowsetta_io.annotation_to_clip_annotation( annotation, recording=recording, ) @@ -294,7 +295,7 @@ def test_bbox_annotation_from_clip_annotation_with_incompatible_geoms( clip_annotation: data.ClipAnnotation, tmp_path: Path, ): - bbox_annotation = io.crowsetta.annotation_from_clip_annotation( + bbox_annotation = crowsetta_io.annotation_from_clip_annotation( clip_annotation, annot_path=tmp_path / "annotation.txt", annotation_fmt="bbox", @@ -310,7 +311,7 @@ def test_bbox_annotation_from_clip_annotation_fails_on_incompatible_geoms( tmp_path: Path, ): with pytest.raises(ValueError): - io.crowsetta.annotation_from_clip_annotation( + crowsetta_io.annotation_from_clip_annotation( clip_annotation, annot_path=tmp_path / "annotation.txt", annotation_fmt="bbox", diff --git a/tests/test_io/test_crowsetta/test_bbox.py b/tests/test_io/test_crowsetta/test_bbox.py index 4cf2573..9935b48 100644 --- a/tests/test_io/test_crowsetta/test_bbox.py +++ b/tests/test_io/test_crowsetta/test_bbox.py @@ -3,7 +3,8 @@ import crowsetta import pytest -from soundevent import data, io +from soundevent import data +import soundevent.io.crowsetta as crowsetta_io @pytest.fixture @@ -52,7 +53,7 @@ def bbox() -> crowsetta.BBox: def test_bbox_from_annotation( sound_event_annotation: data.SoundEventAnnotation, ): - bbox = io.crowsetta.bbox_from_annotation(sound_event_annotation) + bbox = crowsetta_io.bbox_from_annotation(sound_event_annotation) assert isinstance(bbox, crowsetta.BBox) assert bbox.onset == 0.5 assert bbox.low_freq == 0.5 @@ -69,7 +70,7 @@ def test_bbox_from_annotation_fails_on_other_geometries( ) with pytest.raises(ValueError): - io.crowsetta.bbox_from_annotation( + crowsetta_io.bbox_from_annotation( sound_event_annotation, cast_to_bbox=False, ) @@ -86,7 +87,7 @@ def test_bbox_from_annotation_fails_on_empty_geometry( ) with pytest.raises(ValueError): - io.crowsetta.bbox_from_annotation( + crowsetta_io.bbox_from_annotation( sound_event_annotation, cast_to_bbox=False, ) @@ -99,7 +100,7 @@ def test_bbox_from_annotations_can_cast_to_bbox( coordinates=[[0.5, 0.5], [1.5, 1.5]], ) - bbox = io.crowsetta.bbox_from_annotation( + bbox = crowsetta_io.bbox_from_annotation( sound_event_annotation, cast_to_bbox=True, ) @@ -119,7 +120,7 @@ def test_bbox_from_annotation_fails_on_time_geometries( ) with pytest.raises(ValueError): - io.crowsetta.bbox_from_annotation( + crowsetta_io.bbox_from_annotation( sound_event_annotation, cast_to_bbox=True, ) @@ -132,7 +133,7 @@ def test_bbox_from_annotation_cast_time_geometries( coordinates=[0.5, 1.5], ) - bbox = io.crowsetta.bbox_from_annotation( + bbox = crowsetta_io.bbox_from_annotation( sound_event_annotation, cast_to_bbox=True, raise_on_time_geometries=False, @@ -150,7 +151,7 @@ def test_bbox_to_annotation( bbox: crowsetta.BBox, recording: data.Recording, ): - annotation = io.crowsetta.bbox_to_annotation(bbox, recording) + annotation = crowsetta_io.bbox_to_annotation(bbox, recording) assert isinstance(annotation, data.SoundEventAnnotation) geometry = annotation.sound_event.geometry assert isinstance(geometry, data.BoundingBox) @@ -167,7 +168,7 @@ def test_bbox_to_annotation_with_notes_and_created_by( message="random note", created_by=user, ) - annotation = io.crowsetta.bbox_to_annotation( + annotation = crowsetta_io.bbox_to_annotation( bbox, recording, notes=[note], created_by=user ) assert annotation.notes == [note] @@ -178,7 +179,7 @@ def test_bbox_to_annotation_with_time_expanded_recording( bbox: crowsetta.BBox, time_expanded_recording: data.Recording, ): - annotation = io.crowsetta.bbox_to_annotation(bbox, time_expanded_recording) + annotation = crowsetta_io.bbox_to_annotation(bbox, time_expanded_recording) assert annotation.sound_event.recording == time_expanded_recording geometry = annotation.sound_event.geometry assert isinstance(geometry, data.BoundingBox) @@ -189,7 +190,7 @@ def test_bbox_to_annotation_with_time_expanded_recording_no_adjustment( bbox: crowsetta.BBox, time_expanded_recording: data.Recording, ): - annotation = io.crowsetta.bbox_to_annotation( + annotation = crowsetta_io.bbox_to_annotation( bbox, time_expanded_recording, adjust_time_expansion=False, diff --git a/tests/test_io/test_crowsetta/test_import.py b/tests/test_io/test_crowsetta/test_import.py index c171297..b9a1b32 100644 --- a/tests/test_io/test_crowsetta/test_import.py +++ b/tests/test_io/test_crowsetta/test_import.py @@ -8,11 +8,13 @@ import crowsetta -from soundevent import data, io +from soundevent import data +import soundevent.io.crowsetta as crowsetta_io @pytest.mark.skipif( - sys.version_info < (3, 9), reason="requires python3.9 or higher" + sys.version_info < (3, 9), + reason="requires python3.9 or higher", ) def test_can_import_all_example_formats( tmp_path: Path, @@ -64,7 +66,7 @@ def test_can_import_all_example_formats( update=dict(path=annotation.notated_path) ) - clip_annotation = io.crowsetta.annotation_to_clip_annotation( + clip_annotation = crowsetta_io.annotation_to_clip_annotation( annotation, recording=recording, ) diff --git a/tests/test_io/test_crowsetta/test_labels.py b/tests/test_io/test_crowsetta/test_labels.py index 9e5a86d..5316aa4 100644 --- a/tests/test_io/test_crowsetta/test_labels.py +++ b/tests/test_io/test_crowsetta/test_labels.py @@ -1,23 +1,24 @@ """Test Suite for the soundevent.io.crowsetta.labels module""" -from soundevent import data, io +import soundevent.io.crowsetta as crowsetta_io +from soundevent import data def test_label_from_tag_without_kwargs(): tag = data.Tag(key="animal", value="dog") - label = io.crowsetta.label_from_tag(tag) + label = crowsetta_io.label_from_tag(tag) assert label == "animal:dog" def test_label_from_tag_with_only_value(): tag = data.Tag(key="animal", value="dog") - label = io.crowsetta.label_from_tag(tag, value_only=True) + label = crowsetta_io.label_from_tag(tag, value_only=True) assert label == "dog" def test_label_from_tag_with_label_mapping(): tag = data.Tag(key="crowsetta", value="dog") - label = io.crowsetta.label_from_tag( + label = crowsetta_io.label_from_tag( tag, label_mapping={data.Tag(key="crowsetta", value="dog"): "cat"}, ) @@ -26,7 +27,7 @@ def test_label_from_tag_with_label_mapping(): def test_label_from_tag_with_label_mapping_missing_label(): tag = data.Tag(key="crowsetta", value="dog") - label = io.crowsetta.label_from_tag( + label = crowsetta_io.label_from_tag( tag, label_mapping={data.Tag(key="crowsetta", value="cat"): "dog"}, ) @@ -35,7 +36,7 @@ def test_label_from_tag_with_label_mapping_missing_label(): def test_label_from_tag_with_label_fn(): tag = data.Tag(key="crowsetta", value="dog") - label = io.crowsetta.label_from_tag( + label = crowsetta_io.label_from_tag( tag, label_fn=lambda _: "cat", ) @@ -44,7 +45,7 @@ def test_label_from_tag_with_label_fn(): def test_label_from_tag_with_label_fn_and_label_mapping(): tag = data.Tag(key="crowsetta", value="dog") - label = io.crowsetta.label_from_tag( + label = crowsetta_io.label_from_tag( tag, label_fn=lambda _: "cat", # type: ignore label_mapping={data.Tag(key="crowsetta", value="dog"): "bird"}, @@ -54,7 +55,7 @@ def test_label_from_tag_with_label_fn_and_label_mapping(): def test_label_to_tags_with_custom_fn(): label = "crowsetta-dog" - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( label, tag_fn=lambda _: data.Tag(key="crowsetta", value="dog"), ) @@ -63,7 +64,7 @@ def test_label_to_tags_with_custom_fn(): def test_label_to_tags_with_custom_fn_list(): label = "crowsetta-dog" - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( label, tag_fn=lambda _: [data.Tag(key="crowsetta", value="dog")], ) @@ -77,7 +78,7 @@ def failing_fn(label): return data.Tag(key="animal", value=label) label = "dog" - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( label, tag_fn=failing_fn, ) @@ -86,7 +87,7 @@ def failing_fn(label): def test_label_to_tags_with_tag_mapping_single_tag(): label = "crowsetta-dog" - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( label, tag_mapping={"crowsetta-dog": data.Tag(key="crowsetta", value="dog")}, ) @@ -95,7 +96,7 @@ def test_label_to_tags_with_tag_mapping_single_tag(): def test_label_to_tags_with_tag_mapping_tag_list(): label = "crowsetta-dog" - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( label, tag_mapping={"crowsetta-dog": [data.Tag(key="animal", value="dog")]}, ) @@ -104,7 +105,7 @@ def test_label_to_tags_with_tag_mapping_tag_list(): def test_label_to_tags_with_tag_mapping_missing_label(): label = "dog" - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( label, tag_mapping={"cat": data.Tag(key="animal", value="cat")}, ) @@ -113,19 +114,19 @@ def test_label_to_tags_with_tag_mapping_missing_label(): def test_label_to_tags_with_key_mapping(): key_mapping = {"bat": "animal", "female": "sex"} - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( "bat", key_mapping=key_mapping, ) assert tag == [data.Tag(key="animal", value="bat")] - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( "female", key_mapping=key_mapping, ) assert tag == [data.Tag(key="sex", value="female")] - tags = io.crowsetta.label_to_tags( + tags = crowsetta_io.label_to_tags( "large", key_mapping=key_mapping, ) @@ -135,19 +136,19 @@ def test_label_to_tags_with_key_mapping(): def test_label_to_tags_with_key_mapping_fallback(): key_mapping = {"bat": "animal"} - tag = io.crowsetta.label_to_tags( + tag = crowsetta_io.label_to_tags( "dog", key_mapping=key_mapping, fallback="pet" ) assert tag == [data.Tag(key="pet", value="dog")] def test_label_to_tags_with_empty_labels(): - tags = io.crowsetta.label_to_tags( + tags = crowsetta_io.label_to_tags( "__empty__", ) assert tags == [] - tags = io.crowsetta.label_to_tags( + tags = crowsetta_io.label_to_tags( "NA", empty_labels=["NA"], ) @@ -156,7 +157,7 @@ def test_label_to_tags_with_empty_labels(): def test_label_from_tags_with_custom_fn(): tags = [data.Tag(key="animal", value="cat")] - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, seq_label_fn=lambda _: "dog", ) @@ -164,10 +165,10 @@ def test_label_from_tags_with_custom_fn(): def test_label_from_tags_empty_list(): - label = io.crowsetta.label_from_tags([]) + label = crowsetta_io.label_from_tags([]) assert label == "__empty__" - label = io.crowsetta.label_from_tags([], empty_label="NA") + label = crowsetta_io.label_from_tags([], empty_label="NA") assert label == "NA" @@ -176,13 +177,13 @@ def test_label_from_tags_select_by_key(): data.Tag(key="animal", value="dog"), data.Tag(key="sex", value="male"), ] - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, select_by_key="animal", ) assert label == "dog" - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, select_by_key="sex", ) @@ -193,13 +194,13 @@ def test_label_from_tags_select_by_key_missing_key(): tags = [ data.Tag(key="animal", value="dog"), ] - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, select_by_key="sex", ) assert label == "__empty__" - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, select_by_key="sex", empty_label="NA", @@ -212,28 +213,28 @@ def test_label_from_tags_select_by_index(): data.Tag(key="animal", value="dog"), data.Tag(key="sex", value="male"), ] - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, index=0, value_only=True, ) assert label == "dog" - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, index=1, value_only=False, ) assert label == "sex:male" - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, index=2, value_only=False, ) assert label == "animal:dog" - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, index=-1, value_only=True, @@ -247,7 +248,7 @@ def test_label_from_tags_concat_all(): data.Tag(key="sex", value="male"), ] - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, ) assert label == "animal:dog,sex:male" @@ -259,7 +260,7 @@ def test_label_from_tags_with_custom_separator(): data.Tag(key="sex", value="male"), ] - label = io.crowsetta.label_from_tags( + label = crowsetta_io.label_from_tags( tags, separator="|", ) diff --git a/tests/test_io/test_crowsetta/test_segments.py b/tests/test_io/test_crowsetta/test_segments.py index 81fd744..3804d8f 100644 --- a/tests/test_io/test_crowsetta/test_segments.py +++ b/tests/test_io/test_crowsetta/test_segments.py @@ -3,7 +3,8 @@ import crowsetta import pytest -from soundevent import data, io +from soundevent import data +import soundevent.io.crowsetta as crowsetta_io from soundevent.io.crowsetta.segment import ( create_crowsetta_segment, ) @@ -59,7 +60,7 @@ def test_segment_from_annotation_fails_on_empty_geometry( ) with pytest.raises(ValueError): - io.crowsetta.segment_from_annotation(sound_event_annotation) + crowsetta_io.segment_from_annotation(sound_event_annotation) def test_segment_to_annotation_without_onset_and_offset_in_seconds( @@ -71,7 +72,7 @@ def test_segment_to_annotation_without_onset_and_offset_in_seconds( onset_sample=3000, offset_sample=4000, ) - annotation = io.crowsetta.segment_to_annotation(segment, recording) + annotation = crowsetta_io.segment_to_annotation(segment, recording) assert isinstance(annotation, data.SoundEventAnnotation) geometry = annotation.sound_event.geometry assert isinstance(geometry, data.TimeInterval) @@ -84,7 +85,7 @@ def test_segment_to_annotation_without_onset_and_offset_in_seconds( def test_segment_from_annotation( sound_event_annotation: data.SoundEventAnnotation, ): - segment = io.crowsetta.segment_from_annotation(sound_event_annotation) + segment = crowsetta_io.segment_from_annotation(sound_event_annotation) assert isinstance(segment, crowsetta.Segment) assert segment.onset_s == 0.5 assert segment.offset_s == 1.5 @@ -98,7 +99,7 @@ def test_segment_from_annotation_fails_if_not_a_time_interval( coordinates=[0.5, 1] ) with pytest.raises(ValueError): - io.crowsetta.segment_from_annotation( + crowsetta_io.segment_from_annotation( sound_event_annotation, cast_to_segment=False, ) @@ -110,7 +111,7 @@ def test_segment_from_annotation_casts_to_segment( sound_event_annotation.sound_event.geometry = data.Point( coordinates=[0.5, 1] ) - segment = io.crowsetta.segment_from_annotation( + segment = crowsetta_io.segment_from_annotation( sound_event_annotation, cast_to_segment=True, ) @@ -124,7 +125,7 @@ def test_segment_to_annotation( segment: crowsetta.Segment, recording: data.Recording, ): - annotation = io.crowsetta.segment_to_annotation(segment, recording) + annotation = crowsetta_io.segment_to_annotation(segment, recording) assert isinstance(annotation, data.SoundEventAnnotation) geometry = annotation.sound_event.geometry assert isinstance(geometry, data.TimeInterval) @@ -142,7 +143,7 @@ def test_segment_to_annotation_with_notes_and_created_by( message="random note", created_by=user, ) - annotation = io.crowsetta.segment_to_annotation( + annotation = crowsetta_io.segment_to_annotation( segment, recording, created_by=user, @@ -157,7 +158,7 @@ def test_segment_to_annotation_on_time_expanded_recording( segment: crowsetta.Segment, time_expanded_recording: data.Recording, ): - annotation = io.crowsetta.segment_to_annotation( + annotation = crowsetta_io.segment_to_annotation( segment, time_expanded_recording, ) @@ -171,7 +172,7 @@ def test_segment_to_annotation_on_time_expanded_recording_without_adjustment( segment: crowsetta.Segment, time_expanded_recording: data.Recording, ): - annotation = io.crowsetta.segment_to_annotation( + annotation = crowsetta_io.segment_to_annotation( segment, time_expanded_recording, adjust_time_expansion=False, diff --git a/tests/test_io/test_crowsetta/test_sequence.py b/tests/test_io/test_crowsetta/test_sequence.py index fdaa95e..6c4897d 100644 --- a/tests/test_io/test_crowsetta/test_sequence.py +++ b/tests/test_io/test_crowsetta/test_sequence.py @@ -6,7 +6,8 @@ import numpy as np import pytest -from soundevent import data, io +import soundevent.io.crowsetta as crowsetta_io +from soundevent import data from soundevent.io.crowsetta.segment import create_crowsetta_segment @@ -73,7 +74,7 @@ def sequence() -> crowsetta.Sequence: def test_sequence_from_annotations( sound_event_annotations: List[data.SoundEventAnnotation], ): - sequence = io.crowsetta.sequence_from_annotations(sound_event_annotations) + sequence = crowsetta_io.sequence_from_annotations(sound_event_annotations) assert isinstance(sequence, crowsetta.Sequence) assert (sequence.onsets_s == np.array([0.5, 2.0])).all() assert (sequence.offsets_s == np.array([1.5, 2.5])).all() @@ -99,7 +100,7 @@ def test_sequence_from_annotations_fails_on_non_compatible_geometries( ] with pytest.raises(ValueError): - io.crowsetta.sequence_from_annotations( + crowsetta_io.sequence_from_annotations( sound_event_annotations, cast_to_segment=False, ) @@ -123,7 +124,7 @@ def test_sequence_from_annotations_ignores_non_compatible_geometries( ), ] - sequence = io.crowsetta.sequence_from_annotations( + sequence = crowsetta_io.sequence_from_annotations( sound_event_annotations, cast_to_segment=False, ignore_errors=True, @@ -150,7 +151,7 @@ def test_sequence_from_annotation_casts_to_geometry( ), ] - sequence = io.crowsetta.sequence_from_annotations( + sequence = crowsetta_io.sequence_from_annotations( sound_event_annotations, cast_to_segment=True, ) @@ -164,7 +165,7 @@ def test_sequence_to_annotations( sequence: crowsetta.Sequence, recording: data.Recording, ): - annotations = io.crowsetta.sequence_to_annotations( + annotations = crowsetta_io.sequence_to_annotations( sequence, recording, ) diff --git a/tests/test_soundevent.py b/tests/test_soundevent.py index 46a4e17..e073aaa 100644 --- a/tests/test_soundevent.py +++ b/tests/test_soundevent.py @@ -6,3 +6,7 @@ def test_has_version_number(): """Test that soundevent has a version number.""" assert hasattr(soundevent, "__version__") + + +def failing_test(): + raise Exception("This test is supposed to fail.") From fcebf960b10ae61188c97bb52ca31b4aade93f17 Mon Sep 17 00:00:00 2001 From: Santiago Martinez Date: Wed, 7 Feb 2024 14:49:28 +0000 Subject: [PATCH 10/10] Added integration with crowsetta documentation --- .../5_integration_with_crowsetta.py | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 docs/user_guide/5_integration_with_crowsetta.py diff --git a/docs/user_guide/5_integration_with_crowsetta.py b/docs/user_guide/5_integration_with_crowsetta.py new file mode 100644 index 0000000..d79da56 --- /dev/null +++ b/docs/user_guide/5_integration_with_crowsetta.py @@ -0,0 +1,213 @@ +"""# Integrating Crowsetta with Soundevent + +[Crowsetta](https://crowsetta.readthedocs.io/en/latest/) is a versatile Python +tool designed for handling annotations of animal vocalizations and bioacoustics +data. If you're working with diverse annotation formats Crowsetta has you +covered. Soundevent complements this functionality with its +`soundevent.io.crowsetta` module, offering a convenient solution for converting +between Crowsetta and Soundevent formats. + +In this guide, we'll walk through the process of using Crowsetta to load +annotations and then converting them to Soundevent format using the +[`soundevent.io.crowsetta`][soundevent.io.crowsetta] module. + +???+ info "Usage details" + + To use the `soundevent.io.crowsetta` module you need to install some + additional dependencies. You can do this by running the following + command: + + ```bash + pip install soundevent[crowsetta] + ``` + +## Loading annotations with `crowsetta` + +To begin our journey, let's delve into loading annotations using Crowsetta. + +### Crowsetta supported formats + +Crowsetta offers support for various annotation formats. Let's explore the +available formats: + +""" + +import crowsetta + +print(crowsetta.data.available_formats()) + +# %% +# ### Loading Example Raven Annotations +# +# Let's walk through the process of loading example Raven annotations using +# Crowsetta. + +import os +import tempfile + +with tempfile.TemporaryDirectory() as tmpdirname: + # Extract the example data files to a temporary directory + data_dir = os.path.join(tmpdirname, "crowsetta_data") + crowsetta.data.extract_data_files(user_data_dir=data_dir) + + # Select a Raven example file + example_file = crowsetta.data.get("raven", user_data_dir=data_dir) + + # Create a Raven transcriber + transcriber = crowsetta.Transcriber("raven") + + # Load the Raven annotations + # For this example, we assume the annotations correspond to a test audio + # file. + raven_annotations = transcriber.from_file( + example_file.annot_path, + annot_col="Species", + audio_path="sample_audio.wav", + ) + + print(raven_annotations) + + # Convert the annotations to the standard crowsetta format + annotations = raven_annotations.to_annot() + +print(f"Citation: {example_file.citation}") +print(f"Loaded {len(annotations.bboxes)} bounding box annotations") +print("Notated file: ", annotations.notated_path) + +# %% +# ## Converting to Soundevent format +# +# Having successfully loaded the annotations using Crowsetta, we're now ready +# to convert them to Soundevent format. + +import soundevent.io.crowsetta as cr + +# Convert Crowsetta Annotations to Soundevent ClipAnnotation +clip_annotation = cr.annotation_to_clip_annotation(annotations) + +# Print JSON representation of the ClipAnnotation object +print( + clip_annotation.model_dump_json( + indent=2, + # Avoid printing irrelevant information + exclude_none=True, + exclude_defaults=True, + exclude_unset=True, + ) +) + +# %% +# And that's it! We have successfully loaded annotations using `crowsetta` and +# converted them to `soundevent` format. + +# %% +# ## Converting back to `crowsetta` format +# +# Now, let's explore the process of converting Soundevent annotations back to +# Crowsetta objects. + +from soundevent.data import ( + BoundingBox, + Clip, + ClipAnnotation, + Recording, + SoundEvent, + SoundEventAnnotation, + Tag, +) + +# First, let's create some annotations for the example audio file +recording = Recording.from_file("sample_audio.wav") + +clip_annotation = ClipAnnotation( + clip=Clip( + recording=recording, + start_time=0, + end_time=1, + ), + sound_events=[ + SoundEventAnnotation( + tags=[ + Tag(key="species", value="bird"), + Tag(key="color", value="red"), + ], + sound_event=SoundEvent( + recording=recording, + geometry=BoundingBox(coordinates=[0.1, 2000, 0.2, 3000]), + ), + ), + SoundEventAnnotation( + tags=[Tag(key="species", value="frog")], + sound_event=SoundEvent( + recording=recording, + geometry=BoundingBox(coordinates=[0.3, 1000, 0.6, 1500]), + ), + ), + ], +) + +# %% +# Now, let's convert the ClipAnnotation object to Crowsetta format + +annotations = cr.annotation_from_clip_annotation( + clip_annotation, + "random_file_path.txt", + annotation_fmt="bbox", +) + +print(annotations) + +# %% +# !!! note +# +# While working with `crowsetta`, annotation objects are typically loaded +# from a file. In this demonstration, we're using a random file name to +# instantiate the annotations, even though the file doesn't exist. It's +# important to note that `crowsetta` requires a file path to create +# annotations, even if they are not actually written to the file. +# Therefore, using a random filepath is a safe practice. + +# %% +# ## Finer Control +# +# When converting between crowsetta and soundevent formats, you have a +# multitude of options at your disposal. Soundevent objects can contain a +# wealth of information beyond what crowsetta objects offer, including multiple +# tags, notes, various geometry types, and more. Consequently, the conversion +# process isn't always straightforward. Particularly when converting from +# soundevent to crowsetta format, you'll need to make decisions regarding how +# to handle the additional information. + +# %% +# ### Tags and Labels +# +# One of the primary distinctions between crowsetta and soundevent lies in +# their handling of labels/tags. While crowsetta employs a single textual label +# for each annotation, soundevent utilizes a list of key-value tags. This +# difference complicates the conversion process. + +# %% +# By default, when converting to crowsetta format, the label field of the +# crowsetta annotation gets converted to a single tag with the key `crowsetta` +# and the value of the label field. However, numerous customization options are +# available to tailor this behavior. Refer to the +# [documentation][soundevent.io.crowsetta.label_to_tags] for more information. + +label = "bird" +tags = cr.label_to_tags(label) +print(tags) + +# %% +# In the reverse direction, the default behavior amalgamates all tags into a +# single label. For example: + +tags = [ + Tag(key="species", value="bird"), + Tag(key="color", value="red"), +] +label = cr.label_from_tags(tags) +print(label) + +# %% +# Once again, you have the option to customize this behavior. Refer to the +# [documentation][soundevent.io.crowsetta.label_from_tags] for more information.