diff --git a/api/poetry.lock b/api/poetry.lock index e3e9a17..f3d5d9d 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -25,92 +25,92 @@ files = [ [[package]] name = "aiohttp" -version = "3.11.14" +version = "3.11.16" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e2bc827c01f75803de77b134afdbf74fa74b62970eafdf190f3244931d7a5c0d"}, - {file = "aiohttp-3.11.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e365034c5cf6cf74f57420b57682ea79e19eb29033399dd3f40de4d0171998fa"}, - {file = "aiohttp-3.11.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c32593ead1a8c6aabd58f9d7ee706e48beac796bb0cb71d6b60f2c1056f0a65f"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4e7c7ec4146a94a307ca4f112802a8e26d969018fabed526efc340d21d3e7d0"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8b2df9feac55043759aa89f722a967d977d80f8b5865a4153fc41c93b957efc"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7571f99525c76a6280f5fe8e194eeb8cb4da55586c3c61c59c33a33f10cfce7"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b59d096b5537ec7c85954cb97d821aae35cfccce3357a2cafe85660cc6295628"}, - {file = "aiohttp-3.11.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b42dbd097abb44b3f1156b4bf978ec5853840802d6eee2784857be11ee82c6a0"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b05774864c87210c531b48dfeb2f7659407c2dda8643104fb4ae5e2c311d12d9"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4e2e8ef37d4bc110917d038807ee3af82700a93ab2ba5687afae5271b8bc50ff"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e9faafa74dbb906b2b6f3eb9942352e9e9db8d583ffed4be618a89bd71a4e914"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e7abe865504f41b10777ac162c727af14e9f4db9262e3ed8254179053f63e6d"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:4848ae31ad44330b30f16c71e4f586cd5402a846b11264c412de99fa768f00f3"}, - {file = "aiohttp-3.11.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2d0b46abee5b5737cb479cc9139b29f010a37b1875ee56d142aefc10686a390b"}, - {file = "aiohttp-3.11.14-cp310-cp310-win32.whl", hash = "sha256:a0d2c04a623ab83963576548ce098baf711a18e2c32c542b62322a0b4584b990"}, - {file = "aiohttp-3.11.14-cp310-cp310-win_amd64.whl", hash = "sha256:5409a59d5057f2386bb8b8f8bbcfb6e15505cedd8b2445db510563b5d7ea1186"}, - {file = "aiohttp-3.11.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f296d637a50bb15fb6a229fbb0eb053080e703b53dbfe55b1e4bb1c5ed25d325"}, - {file = "aiohttp-3.11.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec6cd1954ca2bbf0970f531a628da1b1338f594bf5da7e361e19ba163ecc4f3b"}, - {file = "aiohttp-3.11.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:572def4aad0a4775af66d5a2b5923c7de0820ecaeeb7987dcbccda2a735a993f"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c68e41c4d576cd6aa6c6d2eddfb32b2acfb07ebfbb4f9da991da26633a3db1a"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b8bbfc8111826aa8363442c0fc1f5751456b008737ff053570f06a151650b3"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b0a200e85da5c966277a402736a96457b882360aa15416bf104ca81e6f5807b"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d173c0ac508a2175f7c9a115a50db5fd3e35190d96fdd1a17f9cb10a6ab09aa1"}, - {file = "aiohttp-3.11.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:413fe39fd929329f697f41ad67936f379cba06fcd4c462b62e5b0f8061ee4a77"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65c75b14ee74e8eeff2886321e76188cbe938d18c85cff349d948430179ad02c"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:321238a42ed463848f06e291c4bbfb3d15ba5a79221a82c502da3e23d7525d06"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:59a05cdc636431f7ce843c7c2f04772437dd816a5289f16440b19441be6511f1"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:daf20d9c3b12ae0fdf15ed92235e190f8284945563c4b8ad95b2d7a31f331cd3"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:05582cb2d156ac7506e68b5eac83179faedad74522ed88f88e5861b78740dc0e"}, - {file = "aiohttp-3.11.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:12c5869e7ddf6b4b1f2109702b3cd7515667b437da90a5a4a50ba1354fe41881"}, - {file = "aiohttp-3.11.14-cp311-cp311-win32.whl", hash = "sha256:92868f6512714efd4a6d6cb2bfc4903b997b36b97baea85f744229f18d12755e"}, - {file = "aiohttp-3.11.14-cp311-cp311-win_amd64.whl", hash = "sha256:bccd2cb7aa5a3bfada72681bdb91637094d81639e116eac368f8b3874620a654"}, - {file = "aiohttp-3.11.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:70ab0f61c1a73d3e0342cedd9a7321425c27a7067bebeeacd509f96695b875fc"}, - {file = "aiohttp-3.11.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:602d4db80daf4497de93cb1ce00b8fc79969c0a7cf5b67bec96fa939268d806a"}, - {file = "aiohttp-3.11.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a8a0d127c10b8d89e69bbd3430da0f73946d839e65fec00ae48ca7916a31948"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9f835cdfedcb3f5947304e85b8ca3ace31eef6346d8027a97f4de5fb687534"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8aa5c68e1e68fff7cd3142288101deb4316b51f03d50c92de6ea5ce646e6c71f"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b512f1de1c688f88dbe1b8bb1283f7fbeb7a2b2b26e743bb2193cbadfa6f307"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc9253069158d57e27d47a8453d8a2c5a370dc461374111b5184cf2f147a3cc3"}, - {file = "aiohttp-3.11.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b2501f1b981e70932b4a552fc9b3c942991c7ae429ea117e8fba57718cdeed0"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:28a3d083819741592685762d51d789e6155411277050d08066537c5edc4066e6"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0df3788187559c262922846087e36228b75987f3ae31dd0a1e5ee1034090d42f"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e73fa341d8b308bb799cf0ab6f55fc0461d27a9fa3e4582755a3d81a6af8c09"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51ba80d473eb780a329d73ac8afa44aa71dfb521693ccea1dea8b9b5c4df45ce"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8d1dd75aa4d855c7debaf1ef830ff2dfcc33f893c7db0af2423ee761ebffd22b"}, - {file = "aiohttp-3.11.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41cf0cefd9e7b5c646c2ef529c8335e7eafd326f444cc1cdb0c47b6bc836f9be"}, - {file = "aiohttp-3.11.14-cp312-cp312-win32.whl", hash = "sha256:948abc8952aff63de7b2c83bfe3f211c727da3a33c3a5866a0e2cf1ee1aa950f"}, - {file = "aiohttp-3.11.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b420d076a46f41ea48e5fcccb996f517af0d406267e31e6716f480a3d50d65c"}, - {file = "aiohttp-3.11.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d14e274828561db91e4178f0057a915f3af1757b94c2ca283cb34cbb6e00b50"}, - {file = "aiohttp-3.11.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f30fc72daf85486cdcdfc3f5e0aea9255493ef499e31582b34abadbfaafb0965"}, - {file = "aiohttp-3.11.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4edcbe34e6dba0136e4cabf7568f5a434d89cc9de5d5155371acda275353d228"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a7169ded15505f55a87f8f0812c94c9412623c744227b9e51083a72a48b68a5"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad1f2fb9fe9b585ea4b436d6e998e71b50d2b087b694ab277b30e060c434e5db"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20412c7cc3720e47a47e63c0005f78c0c2370020f9f4770d7fc0075f397a9fb0"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd9766da617855f7e85f27d2bf9a565ace04ba7c387323cd3e651ac4329db91"}, - {file = "aiohttp-3.11.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:599b66582f7276ebefbaa38adf37585e636b6a7a73382eb412f7bc0fc55fb73d"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b41693b7388324b80f9acfabd479bd1c84f0bc7e8f17bab4ecd9675e9ff9c734"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:86135c32d06927339c8c5e64f96e4eee8825d928374b9b71a3c42379d7437058"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04eb541ce1e03edc1e3be1917a0f45ac703e913c21a940111df73a2c2db11d73"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dc311634f6f28661a76cbc1c28ecf3b3a70a8edd67b69288ab7ca91058eb5a33"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69bb252bfdca385ccabfd55f4cd740d421dd8c8ad438ded9637d81c228d0da49"}, - {file = "aiohttp-3.11.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2b86efe23684b58a88e530c4ab5b20145f102916bbb2d82942cafec7bd36a647"}, - {file = "aiohttp-3.11.14-cp313-cp313-win32.whl", hash = "sha256:b9c60d1de973ca94af02053d9b5111c4fbf97158e139b14f1be68337be267be6"}, - {file = "aiohttp-3.11.14-cp313-cp313-win_amd64.whl", hash = "sha256:0a29be28e60e5610d2437b5b2fed61d6f3dcde898b57fb048aa5079271e7f6f3"}, - {file = "aiohttp-3.11.14-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:14fc03508359334edc76d35b2821832f092c8f092e4b356e74e38419dfe7b6de"}, - {file = "aiohttp-3.11.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:92007c89a8cb7be35befa2732b0b32bf3a394c1b22ef2dff0ef12537d98a7bda"}, - {file = "aiohttp-3.11.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6d3986112e34eaa36e280dc8286b9dd4cc1a5bcf328a7f147453e188f6fe148f"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:749f1eb10e51dbbcdba9df2ef457ec060554842eea4d23874a3e26495f9e87b1"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:781c8bd423dcc4641298c8c5a2a125c8b1c31e11f828e8d35c1d3a722af4c15a"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:997b57e38aa7dc6caab843c5e042ab557bc83a2f91b7bd302e3c3aebbb9042a1"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a8b0321e40a833e381d127be993b7349d1564b756910b28b5f6588a159afef3"}, - {file = "aiohttp-3.11.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8778620396e554b758b59773ab29c03b55047841d8894c5e335f12bfc45ebd28"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e906da0f2bcbf9b26cc2b144929e88cb3bf943dd1942b4e5af066056875c7618"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:87f0e003fb4dd5810c7fbf47a1239eaa34cd929ef160e0a54c570883125c4831"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7f2dadece8b85596ac3ab1ec04b00694bdd62abc31e5618f524648d18d9dd7fa"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:fe846f0a98aa9913c2852b630cd39b4098f296e0907dd05f6c7b30d911afa4c3"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ced66c5c6ad5bcaf9be54560398654779ec1c3695f1a9cf0ae5e3606694a000a"}, - {file = "aiohttp-3.11.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a40087b82f83bd671cbeb5f582c233d196e9653220404a798798bfc0ee189fff"}, - {file = "aiohttp-3.11.14-cp39-cp39-win32.whl", hash = "sha256:95d7787f2bcbf7cb46823036a8d64ccfbc2ffc7d52016b4044d901abceeba3db"}, - {file = "aiohttp-3.11.14-cp39-cp39-win_amd64.whl", hash = "sha256:22a8107896877212130c58f74e64b77f7007cb03cea8698be317272643602d45"}, - {file = "aiohttp-3.11.14.tar.gz", hash = "sha256:d6edc538c7480fa0a3b2bdd705f8010062d74700198da55d16498e1b49549b9c"}, + {file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb46bb0f24813e6cede6cc07b1961d4b04f331f7112a23b5e21f567da4ee50aa"}, + {file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54eb3aead72a5c19fad07219acd882c1643a1027fbcdefac9b502c267242f955"}, + {file = "aiohttp-3.11.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:38bea84ee4fe24ebcc8edeb7b54bf20f06fd53ce4d2cc8b74344c5b9620597fd"}, + {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0666afbe984f6933fe72cd1f1c3560d8c55880a0bdd728ad774006eb4241ecd"}, + {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba92a2d9ace559a0a14b03d87f47e021e4fa7681dc6970ebbc7b447c7d4b7cd"}, + {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ad1d59fd7114e6a08c4814983bb498f391c699f3c78712770077518cae63ff7"}, + {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b88a2bf26965f2015a771381624dd4b0839034b70d406dc74fd8be4cc053e3"}, + {file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:576f5ca28d1b3276026f7df3ec841ae460e0fc3aac2a47cbf72eabcfc0f102e1"}, + {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a2a450bcce4931b295fc0848f384834c3f9b00edfc2150baafb4488c27953de6"}, + {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:37dcee4906454ae377be5937ab2a66a9a88377b11dd7c072df7a7c142b63c37c"}, + {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4d0c970c0d602b1017e2067ff3b7dac41c98fef4f7472ec2ea26fd8a4e8c2149"}, + {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:004511d3413737700835e949433536a2fe95a7d0297edd911a1e9705c5b5ea43"}, + {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c15b2271c44da77ee9d822552201180779e5e942f3a71fb74e026bf6172ff287"}, + {file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad9509ffb2396483ceacb1eee9134724443ee45b92141105a4645857244aecc8"}, + {file = "aiohttp-3.11.16-cp310-cp310-win32.whl", hash = "sha256:634d96869be6c4dc232fc503e03e40c42d32cfaa51712aee181e922e61d74814"}, + {file = "aiohttp-3.11.16-cp310-cp310-win_amd64.whl", hash = "sha256:938f756c2b9374bbcc262a37eea521d8a0e6458162f2a9c26329cc87fdf06534"}, + {file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8cb0688a8d81c63d716e867d59a9ccc389e97ac7037ebef904c2b89334407180"}, + {file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ad1fb47da60ae1ddfb316f0ff16d1f3b8e844d1a1e154641928ea0583d486ed"}, + {file = "aiohttp-3.11.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df7db76400bf46ec6a0a73192b14c8295bdb9812053f4fe53f4e789f3ea66bbb"}, + {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc3a145479a76ad0ed646434d09216d33d08eef0d8c9a11f5ae5cdc37caa3540"}, + {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d007aa39a52d62373bd23428ba4a2546eed0e7643d7bf2e41ddcefd54519842c"}, + {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6ddd90d9fb4b501c97a4458f1c1720e42432c26cb76d28177c5b5ad4e332601"}, + {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a2f451849e6b39e5c226803dcacfa9c7133e9825dcefd2f4e837a2ec5a3bb98"}, + {file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8df6612df74409080575dca38a5237282865408016e65636a76a2eb9348c2567"}, + {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78e6e23b954644737e385befa0deb20233e2dfddf95dd11e9db752bdd2a294d3"}, + {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:696ef00e8a1f0cec5e30640e64eca75d8e777933d1438f4facc9c0cdf288a810"}, + {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3538bc9fe1b902bef51372462e3d7c96fce2b566642512138a480b7adc9d508"}, + {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ab3367bb7f61ad18793fea2ef71f2d181c528c87948638366bf1de26e239183"}, + {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:56a3443aca82abda0e07be2e1ecb76a050714faf2be84256dae291182ba59049"}, + {file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:61c721764e41af907c9d16b6daa05a458f066015abd35923051be8705108ed17"}, + {file = "aiohttp-3.11.16-cp311-cp311-win32.whl", hash = "sha256:3e061b09f6fa42997cf627307f220315e313ece74907d35776ec4373ed718b86"}, + {file = "aiohttp-3.11.16-cp311-cp311-win_amd64.whl", hash = "sha256:745f1ed5e2c687baefc3c5e7b4304e91bf3e2f32834d07baaee243e349624b24"}, + {file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27"}, + {file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713"}, + {file = "aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb"}, + {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321"}, + {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e"}, + {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c"}, + {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce"}, + {file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e"}, + {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b"}, + {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540"}, + {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b"}, + {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e"}, + {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c"}, + {file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71"}, + {file = "aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2"}, + {file = "aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682"}, + {file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489"}, + {file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50"}, + {file = "aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133"}, + {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0"}, + {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca"}, + {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d"}, + {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb"}, + {file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4"}, + {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7"}, + {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd"}, + {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f"}, + {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd"}, + {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34"}, + {file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913"}, + {file = "aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979"}, + {file = "aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802"}, + {file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bbcba75fe879ad6fd2e0d6a8d937f34a571f116a0e4db37df8079e738ea95c71"}, + {file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:87a6e922b2b2401e0b0cf6b976b97f11ec7f136bfed445e16384fbf6fd5e8602"}, + {file = "aiohttp-3.11.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccf10f16ab498d20e28bc2b5c1306e9c1512f2840f7b6a67000a517a4b37d5ee"}, + {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb3d0cc5cdb926090748ea60172fa8a213cec728bd6c54eae18b96040fcd6227"}, + {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d07502cc14ecd64f52b2a74ebbc106893d9a9717120057ea9ea1fd6568a747e7"}, + {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:776c8e959a01e5e8321f1dec77964cb6101020a69d5a94cd3d34db6d555e01f7"}, + {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0902e887b0e1d50424112f200eb9ae3dfed6c0d0a19fc60f633ae5a57c809656"}, + {file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87fd812899aa78252866ae03a048e77bd11b80fb4878ce27c23cade239b42b2"}, + {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a950c2eb8ff17361abd8c85987fd6076d9f47d040ebffce67dce4993285e973"}, + {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c10d85e81d0b9ef87970ecbdbfaeec14a361a7fa947118817fcea8e45335fa46"}, + {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7951decace76a9271a1ef181b04aa77d3cc309a02a51d73826039003210bdc86"}, + {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14461157d8426bcb40bd94deb0450a6fa16f05129f7da546090cebf8f3123b0f"}, + {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9756d9b9d4547e091f99d554fbba0d2a920aab98caa82a8fb3d3d9bee3c9ae85"}, + {file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:87944bd16b7fe6160607f6a17808abd25f17f61ae1e26c47a491b970fb66d8cb"}, + {file = "aiohttp-3.11.16-cp39-cp39-win32.whl", hash = "sha256:92b7ee222e2b903e0a4b329a9943d432b3767f2d5029dbe4ca59fb75223bbe2e"}, + {file = "aiohttp-3.11.16-cp39-cp39-win_amd64.whl", hash = "sha256:17ae4664031aadfbcb34fd40ffd90976671fa0c0286e6c4113989f78bebab37a"}, + {file = "aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8"}, ] [package.dependencies] @@ -239,13 +239,13 @@ type-checking = ["mypy (>=1.9,<2.0)", "types-docutils (>=0.20,<0.21)", "typing-e [[package]] name = "azure-core" -version = "1.32.0" +version = "1.33.0" description = "Microsoft Azure Core Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure_core-1.32.0-py3-none-any.whl", hash = "sha256:eac191a0efb23bfa83fddf321b27b122b4ec847befa3091fa736a5c32c50d7b4"}, - {file = "azure_core-1.32.0.tar.gz", hash = "sha256:22b3c35d6b2dae14990f6c1be2912bf23ffe50b220e708a28ab1bb92b1c730e5"}, + {file = "azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f"}, + {file = "azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9"}, ] [package.dependencies] @@ -255,6 +255,7 @@ typing-extensions = ">=4.6.0" [package.extras] aio = ["aiohttp (>=3.0)"] +tracing = ["opentelemetry-api (>=1.26,<2.0)"] [[package]] name = "azure-cosmos" @@ -696,74 +697,74 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "coverage" -version = "7.7.1" +version = "7.8.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ - {file = "coverage-7.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:553ba93f8e3c70e1b0031e4dfea36aba4e2b51fe5770db35e99af8dc5c5a9dfe"}, - {file = "coverage-7.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44683f2556a56c9a6e673b583763096b8efbd2df022b02995609cf8e64fc8ae0"}, - {file = "coverage-7.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02fad4f8faa4153db76f9246bc95c1d99f054f4e0a884175bff9155cf4f856cb"}, - {file = "coverage-7.7.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c181ceba2e6808ede1e964f7bdc77bd8c7eb62f202c63a48cc541e5ffffccb6"}, - {file = "coverage-7.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80b5b207a8b08c6a934b214e364cab2fa82663d4af18981a6c0a9e95f8df7602"}, - {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:25fe40967717bad0ce628a0223f08a10d54c9d739e88c9cbb0f77b5959367542"}, - {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:881cae0f9cbd928c9c001487bb3dcbfd0b0af3ef53ae92180878591053be0cb3"}, - {file = "coverage-7.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90e9141e9221dd6fbc16a2727a5703c19443a8d9bf7d634c792fa0287cee1ab"}, - {file = "coverage-7.7.1-cp310-cp310-win32.whl", hash = "sha256:ae13ed5bf5542d7d4a0a42ff5160e07e84adc44eda65ddaa635c484ff8e55917"}, - {file = "coverage-7.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:171e9977c6a5d2b2be9efc7df1126fd525ce7cad0eb9904fe692da007ba90d81"}, - {file = "coverage-7.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1165490be0069e34e4f99d08e9c5209c463de11b471709dfae31e2a98cbd49fd"}, - {file = "coverage-7.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:44af11c00fd3b19b8809487630f8a0039130d32363239dfd15238e6d37e41a48"}, - {file = "coverage-7.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fbba59022e7c20124d2f520842b75904c7b9f16c854233fa46575c69949fb5b9"}, - {file = "coverage-7.7.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af94fb80e4f159f4d93fb411800448ad87b6039b0500849a403b73a0d36bb5ae"}, - {file = "coverage-7.7.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae79f8e3501133aa0e220bbc29573910d096795882a70e6f6e6637b09522133"}, - {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e33426a5e1dc7743dd54dfd11d3a6c02c5d127abfaa2edd80a6e352b58347d1a"}, - {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b559adc22486937786731dac69e57296cb9aede7e2687dfc0d2696dbd3b1eb6b"}, - {file = "coverage-7.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b838a91e84e1773c3436f6cc6996e000ed3ca5721799e7789be18830fad009a2"}, - {file = "coverage-7.7.1-cp311-cp311-win32.whl", hash = "sha256:2c492401bdb3a85824669d6a03f57b3dfadef0941b8541f035f83bbfc39d4282"}, - {file = "coverage-7.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:1e6f867379fd033a0eeabb1be0cffa2bd660582b8b0c9478895c509d875a9d9e"}, - {file = "coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d"}, - {file = "coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f"}, - {file = "coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1"}, - {file = "coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0"}, - {file = "coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2"}, - {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af"}, - {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59"}, - {file = "coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2"}, - {file = "coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8"}, - {file = "coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04"}, - {file = "coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585"}, - {file = "coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c"}, - {file = "coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019"}, - {file = "coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf"}, - {file = "coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b"}, - {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f"}, - {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777"}, - {file = "coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a"}, - {file = "coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a"}, - {file = "coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1"}, - {file = "coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511"}, - {file = "coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24"}, - {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950"}, - {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d"}, - {file = "coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e"}, - {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862"}, - {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271"}, - {file = "coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de"}, - {file = "coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c"}, - {file = "coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c"}, - {file = "coverage-7.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34a3bf6b92e6621fc4dcdaab353e173ccb0ca9e4bfbcf7e49a0134c86c9cd303"}, - {file = "coverage-7.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6874929d624d3a670f676efafbbc747f519a6121b581dd41d012109e70a5ebd"}, - {file = "coverage-7.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ba5ff236c87a7b7aa1441a216caf44baee14cbfbd2256d306f926d16b026578"}, - {file = "coverage-7.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452735fafe8ff5918236d5fe1feac322b359e57692269c75151f9b4ee4b7e1bc"}, - {file = "coverage-7.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5f99a93cecf799738e211f9746dc83749b5693538fbfac279a61682ba309387"}, - {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:11dd6f52c2a7ce8bf0a5f3b6e4a8eb60e157ffedc3c4b4314a41c1dfbd26ce58"}, - {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:b52edb940d087e2a96e73c1523284a2e94a4e66fa2ea1e2e64dddc67173bad94"}, - {file = "coverage-7.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d2e73e2ac468536197e6b3ab79bc4a5c9da0f078cd78cfcc7fe27cf5d1195ef0"}, - {file = "coverage-7.7.1-cp39-cp39-win32.whl", hash = "sha256:18f544356bceef17cc55fcf859e5664f06946c1b68efcea6acdc50f8f6a6e776"}, - {file = "coverage-7.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:d66ff48ab3bb6f762a153e29c0fc1eb5a62a260217bc64470d7ba602f5886d20"}, - {file = "coverage-7.7.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:5b7b02e50d54be6114cc4f6a3222fec83164f7c42772ba03b520138859b5fde1"}, - {file = "coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31"}, - {file = "coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, + {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, + {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, + {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, + {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, + {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, + {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, + {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, + {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, + {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, + {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, + {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, + {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, + {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, + {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, + {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, + {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, + {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, + {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, + {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, + {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, + {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, + {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, + {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, + {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, + {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, + {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, + {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, + {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, + {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, + {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, + {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, + {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, + {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, ] [package.dependencies] @@ -919,61 +920,61 @@ typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "fonttools" -version = "4.56.0" +version = "4.57.0" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000"}, - {file = "fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16"}, - {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311"}, - {file = "fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc"}, - {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f"}, - {file = "fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086"}, - {file = "fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786"}, - {file = "fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685"}, - {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df"}, - {file = "fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c"}, - {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c"}, - {file = "fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049"}, - {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62"}, - {file = "fonttools-4.56.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0"}, - {file = "fonttools-4.56.0-cp311-cp311-win32.whl", hash = "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b"}, - {file = "fonttools-4.56.0-cp311-cp311-win_amd64.whl", hash = "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05"}, - {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9"}, - {file = "fonttools-4.56.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f"}, - {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2"}, - {file = "fonttools-4.56.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563"}, - {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a"}, - {file = "fonttools-4.56.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28"}, - {file = "fonttools-4.56.0-cp312-cp312-win32.whl", hash = "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c"}, - {file = "fonttools-4.56.0-cp312-cp312-win_amd64.whl", hash = "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba"}, - {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692"}, - {file = "fonttools-4.56.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0"}, - {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1"}, - {file = "fonttools-4.56.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea"}, - {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3"}, - {file = "fonttools-4.56.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278"}, - {file = "fonttools-4.56.0-cp313-cp313-win32.whl", hash = "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f"}, - {file = "fonttools-4.56.0-cp313-cp313-win_amd64.whl", hash = "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6"}, - {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3fd3fccb7b9adaaecfa79ad51b759f2123e1aba97f857936ce044d4f029abd71"}, - {file = "fonttools-4.56.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:193b86e9f769320bc98ffdb42accafb5d0c8c49bd62884f1c0702bc598b3f0a2"}, - {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e81c1cc80c1d8bf071356cc3e0e25071fbba1c75afc48d41b26048980b3c771"}, - {file = "fonttools-4.56.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9270505a19361e81eecdbc2c251ad1e1a9a9c2ad75fa022ccdee533f55535dc"}, - {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:53f5e9767978a4daf46f28e09dbeb7d010319924ae622f7b56174b777258e5ba"}, - {file = "fonttools-4.56.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9da650cb29bc098b8cfd15ef09009c914b35c7986c8fa9f08b51108b7bc393b4"}, - {file = "fonttools-4.56.0-cp38-cp38-win32.whl", hash = "sha256:965d0209e6dbdb9416100123b6709cb13f5232e2d52d17ed37f9df0cc31e2b35"}, - {file = "fonttools-4.56.0-cp38-cp38-win_amd64.whl", hash = "sha256:654ac4583e2d7c62aebc6fc6a4c6736f078f50300e18aa105d87ce8925cfac31"}, - {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ca7962e8e5fc047cc4e59389959843aafbf7445b6c08c20d883e60ced46370a5"}, - {file = "fonttools-4.56.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1af375734018951c31c0737d04a9d5fd0a353a0253db5fbed2ccd44eac62d8c"}, - {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:442ad4122468d0e47d83bc59d0e91b474593a8c813839e1872e47c7a0cb53b10"}, - {file = "fonttools-4.56.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf4f8d2a30b454ac682e12c61831dcb174950c406011418e739de592bbf8f76"}, - {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:96a4271f63a615bcb902b9f56de00ea225d6896052c49f20d0c91e9f43529a29"}, - {file = "fonttools-4.56.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6c1d38642ca2dddc7ae992ef5d026e5061a84f10ff2b906be5680ab089f55bb8"}, - {file = "fonttools-4.56.0-cp39-cp39-win32.whl", hash = "sha256:2d351275f73ebdd81dd5b09a8b8dac7a30f29a279d41e1c1192aedf1b6dced40"}, - {file = "fonttools-4.56.0-cp39-cp39-win_amd64.whl", hash = "sha256:d6ca96d1b61a707ba01a43318c9c40aaf11a5a568d1e61146fafa6ab20890793"}, - {file = "fonttools-4.56.0-py3-none-any.whl", hash = "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14"}, - {file = "fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4"}, + {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:babe8d1eb059a53e560e7bf29f8e8f4accc8b6cfb9b5fd10e485bde77e71ef41"}, + {file = "fonttools-4.57.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81aa97669cd726349eb7bd43ca540cf418b279ee3caba5e2e295fb4e8f841c02"}, + {file = "fonttools-4.57.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0e9618630edd1910ad4f07f60d77c184b2f572c8ee43305ea3265675cbbfe7e"}, + {file = "fonttools-4.57.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34687a5d21f1d688d7d8d416cb4c5b9c87fca8a1797ec0d74b9fdebfa55c09ab"}, + {file = "fonttools-4.57.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69ab81b66ebaa8d430ba56c7a5f9abe0183afefd3a2d6e483060343398b13fb1"}, + {file = "fonttools-4.57.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d639397de852f2ccfb3134b152c741406752640a266d9c1365b0f23d7b88077f"}, + {file = "fonttools-4.57.0-cp310-cp310-win32.whl", hash = "sha256:cc066cb98b912f525ae901a24cd381a656f024f76203bc85f78fcc9e66ae5aec"}, + {file = "fonttools-4.57.0-cp310-cp310-win_amd64.whl", hash = "sha256:7a64edd3ff6a7f711a15bd70b4458611fb240176ec11ad8845ccbab4fe6745db"}, + {file = "fonttools-4.57.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3871349303bdec958360eedb619169a779956503ffb4543bb3e6211e09b647c4"}, + {file = "fonttools-4.57.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c59375e85126b15a90fcba3443eaac58f3073ba091f02410eaa286da9ad80ed8"}, + {file = "fonttools-4.57.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967b65232e104f4b0f6370a62eb33089e00024f2ce143aecbf9755649421c683"}, + {file = "fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39acf68abdfc74e19de7485f8f7396fa4d2418efea239b7061d6ed6a2510c746"}, + {file = "fonttools-4.57.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d077f909f2343daf4495ba22bb0e23b62886e8ec7c109ee8234bdbd678cf344"}, + {file = "fonttools-4.57.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:46370ac47a1e91895d40e9ad48effbe8e9d9db1a4b80888095bc00e7beaa042f"}, + {file = "fonttools-4.57.0-cp311-cp311-win32.whl", hash = "sha256:ca2aed95855506b7ae94e8f1f6217b7673c929e4f4f1217bcaa236253055cb36"}, + {file = "fonttools-4.57.0-cp311-cp311-win_amd64.whl", hash = "sha256:17168a4670bbe3775f3f3f72d23ee786bd965395381dfbb70111e25e81505b9d"}, + {file = "fonttools-4.57.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:889e45e976c74abc7256d3064aa7c1295aa283c6bb19810b9f8b604dfe5c7f31"}, + {file = "fonttools-4.57.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0425c2e052a5f1516c94e5855dbda706ae5a768631e9fcc34e57d074d1b65b92"}, + {file = "fonttools-4.57.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44c26a311be2ac130f40a96769264809d3b0cb297518669db437d1cc82974888"}, + {file = "fonttools-4.57.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c41ba992df5b8d680b89fd84c6a1f2aca2b9f1ae8a67400c8930cd4ea115f6"}, + {file = "fonttools-4.57.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea1e9e43ca56b0c12440a7c689b1350066595bebcaa83baad05b8b2675129d98"}, + {file = "fonttools-4.57.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84fd56c78d431606332a0627c16e2a63d243d0d8b05521257d77c6529abe14d8"}, + {file = "fonttools-4.57.0-cp312-cp312-win32.whl", hash = "sha256:f4376819c1c778d59e0a31db5dc6ede854e9edf28bbfa5b756604727f7f800ac"}, + {file = "fonttools-4.57.0-cp312-cp312-win_amd64.whl", hash = "sha256:57e30241524879ea10cdf79c737037221f77cc126a8cdc8ff2c94d4a522504b9"}, + {file = "fonttools-4.57.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:408ce299696012d503b714778d89aa476f032414ae57e57b42e4b92363e0b8ef"}, + {file = "fonttools-4.57.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bbceffc80aa02d9e8b99f2a7491ed8c4a783b2fc4020119dc405ca14fb5c758c"}, + {file = "fonttools-4.57.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f022601f3ee9e1f6658ed6d184ce27fa5216cee5b82d279e0f0bde5deebece72"}, + {file = "fonttools-4.57.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dea5893b58d4637ffa925536462ba626f8a1b9ffbe2f5c272cdf2c6ebadb817"}, + {file = "fonttools-4.57.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dff02c5c8423a657c550b48231d0a48d7e2b2e131088e55983cfe74ccc2c7cc9"}, + {file = "fonttools-4.57.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:767604f244dc17c68d3e2dbf98e038d11a18abc078f2d0f84b6c24571d9c0b13"}, + {file = "fonttools-4.57.0-cp313-cp313-win32.whl", hash = "sha256:8e2e12d0d862f43d51e5afb8b9751c77e6bec7d2dc00aad80641364e9df5b199"}, + {file = "fonttools-4.57.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1d6bc9c23356908db712d282acb3eebd4ae5ec6d8b696aa40342b1d84f8e9e3"}, + {file = "fonttools-4.57.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9d57b4e23ebbe985125d3f0cabbf286efa191ab60bbadb9326091050d88e8213"}, + {file = "fonttools-4.57.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:579ba873d7f2a96f78b2e11028f7472146ae181cae0e4d814a37a09e93d5c5cc"}, + {file = "fonttools-4.57.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3e1ec10c29bae0ea826b61f265ec5c858c5ba2ce2e69a71a62f285cf8e4595"}, + {file = "fonttools-4.57.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1968f2a2003c97c4ce6308dc2498d5fd4364ad309900930aa5a503c9851aec8"}, + {file = "fonttools-4.57.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:aff40f8ac6763d05c2c8f6d240c6dac4bb92640a86d9b0c3f3fff4404f34095c"}, + {file = "fonttools-4.57.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d07f1b64008e39fceae7aa99e38df8385d7d24a474a8c9872645c4397b674481"}, + {file = "fonttools-4.57.0-cp38-cp38-win32.whl", hash = "sha256:51d8482e96b28fb28aa8e50b5706f3cee06de85cbe2dce80dbd1917ae22ec5a6"}, + {file = "fonttools-4.57.0-cp38-cp38-win_amd64.whl", hash = "sha256:03290e818782e7edb159474144fca11e36a8ed6663d1fcbd5268eb550594fd8e"}, + {file = "fonttools-4.57.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7339e6a3283e4b0ade99cade51e97cde3d54cd6d1c3744459e886b66d630c8b3"}, + {file = "fonttools-4.57.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:05efceb2cb5f6ec92a4180fcb7a64aa8d3385fd49cfbbe459350229d1974f0b1"}, + {file = "fonttools-4.57.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a97bb05eb24637714a04dee85bdf0ad1941df64fe3b802ee4ac1c284a5f97b7c"}, + {file = "fonttools-4.57.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:541cb48191a19ceb1a2a4b90c1fcebd22a1ff7491010d3cf840dd3a68aebd654"}, + {file = "fonttools-4.57.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:cdef9a056c222d0479a1fdb721430f9efd68268014c54e8166133d2643cb05d9"}, + {file = "fonttools-4.57.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3cf97236b192a50a4bf200dc5ba405aa78d4f537a2c6e4c624bb60466d5b03bd"}, + {file = "fonttools-4.57.0-cp39-cp39-win32.whl", hash = "sha256:e952c684274a7714b3160f57ec1d78309f955c6335c04433f07d36c5eb27b1f9"}, + {file = "fonttools-4.57.0-cp39-cp39-win_amd64.whl", hash = "sha256:a2a722c0e4bfd9966a11ff55c895c817158fcce1b2b6700205a376403b546ad9"}, + {file = "fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f"}, + {file = "fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de"}, ] [package.extras] @@ -1655,103 +1656,103 @@ typing-extensions = {version = "*", markers = "python_version < \"3.11\""} [[package]] name = "multidict" -version = "6.2.0" +version = "6.3.2" description = "multidict implementation" optional = false python-versions = ">=3.9" files = [ - {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b9f6392d98c0bd70676ae41474e2eecf4c7150cb419237a41f8f96043fcb81d1"}, - {file = "multidict-6.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3501621d5e86f1a88521ea65d5cad0a0834c77b26f193747615b7c911e5422d2"}, - {file = "multidict-6.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32ed748ff9ac682eae7859790d3044b50e3076c7d80e17a44239683769ff485e"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc826b9a8176e686b67aa60fd6c6a7047b0461cae5591ea1dc73d28f72332a8a"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:214207dcc7a6221d9942f23797fe89144128a71c03632bf713d918db99bd36de"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05fefbc3cddc4e36da209a5e49f1094bbece9a581faa7f3589201fd95df40e5d"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e851e6363d0dbe515d8de81fd544a2c956fdec6f8a049739562286727d4a00c3"}, - {file = "multidict-6.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32c9b4878f48be3e75808ea7e499d6223b1eea6d54c487a66bc10a1871e3dc6a"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7243c5a6523c5cfeca76e063efa5f6a656d1d74c8b1fc64b2cd1e84e507f7e2a"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0e5a644e50ef9fb87878d4d57907f03a12410d2aa3b93b3acdf90a741df52c49"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0dc25a3293c50744796e87048de5e68996104d86d940bb24bc3ec31df281b191"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a49994481b99cd7dedde07f2e7e93b1d86c01c0fca1c32aded18f10695ae17eb"}, - {file = "multidict-6.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:641cf2e3447c9ecff2f7aa6e9eee9eaa286ea65d57b014543a4911ff2799d08a"}, - {file = "multidict-6.2.0-cp310-cp310-win32.whl", hash = "sha256:0c383d28857f66f5aebe3e91d6cf498da73af75fbd51cedbe1adfb85e90c0460"}, - {file = "multidict-6.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:a33273a541f1e1a8219b2a4ed2de355848ecc0254264915b9290c8d2de1c74e1"}, - {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84e87a7d75fa36839a3a432286d719975362d230c70ebfa0948549cc38bd5b46"}, - {file = "multidict-6.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8de4d42dffd5ced9117af2ce66ba8722402541a3aa98ffdf78dde92badb68932"}, - {file = "multidict-6.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d91a230c7f8af86c904a5a992b8c064b66330544693fd6759c3d6162382ecf"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f6cad071960ba1914fa231677d21b1b4a3acdcce463cee41ea30bc82e6040cf"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f74f2fc51555f4b037ef278efc29a870d327053aba5cb7d86ae572426c7cccc"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14ed9ed1bfedd72a877807c71113deac292bf485159a29025dfdc524c326f3e1"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac3fcf9a2d369bd075b2c2965544036a27ccd277fc3c04f708338cc57533081"}, - {file = "multidict-6.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fc6af8e39f7496047c7876314f4317736eac82bf85b54c7c76cf1a6f8e35d98"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8cb1329f42fadfb40d6211e5ff568d71ab49be36e759345f91c69d1033d633"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5389445f0173c197f4a3613713b5fb3f3879df1ded2a1a2e4bc4b5b9c5441b7e"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:94a7bb972178a8bfc4055db80c51efd24baefaced5e51c59b0d598a004e8305d"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da51d8928ad8b4244926fe862ba1795f0b6e68ed8c42cd2f822d435db9c2a8f4"}, - {file = "multidict-6.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:063be88bd684782a0715641de853e1e58a2f25b76388538bd62d974777ce9bc2"}, - {file = "multidict-6.2.0-cp311-cp311-win32.whl", hash = "sha256:52b05e21ff05729fbea9bc20b3a791c3c11da61649ff64cce8257c82a020466d"}, - {file = "multidict-6.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1e2a2193d3aa5cbf5758f6d5680a52aa848e0cf611da324f71e5e48a9695cc86"}, - {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:437c33561edb6eb504b5a30203daf81d4a9b727e167e78b0854d9a4e18e8950b"}, - {file = "multidict-6.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9f49585f4abadd2283034fc605961f40c638635bc60f5162276fec075f2e37a4"}, - {file = "multidict-6.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5dd7106d064d05896ce28c97da3f46caa442fe5a43bc26dfb258e90853b39b44"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e25b11a0417475f093d0f0809a149aff3943c2c56da50fdf2c3c88d57fe3dfbd"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac380cacdd3b183338ba63a144a34e9044520a6fb30c58aa14077157a033c13e"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61d5541f27533f803a941d3a3f8a3d10ed48c12cf918f557efcbf3cd04ef265c"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:facaf11f21f3a4c51b62931feb13310e6fe3475f85e20d9c9fdce0d2ea561b87"}, - {file = "multidict-6.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:095a2eabe8c43041d3e6c2cb8287a257b5f1801c2d6ebd1dd877424f1e89cf29"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0cc398350ef31167e03f3ca7c19313d4e40a662adcb98a88755e4e861170bdd"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7c611345bbe7cb44aabb877cb94b63e86f2d0db03e382667dbd037866d44b4f8"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd1a0644ccaf27e9d2f6d9c9474faabee21f0578fe85225cc5af9a61e1653df"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:89b3857652183b8206a891168af47bac10b970d275bba1f6ee46565a758c078d"}, - {file = "multidict-6.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:125dd82b40f8c06d08d87b3510beaccb88afac94e9ed4a6f6c71362dc7dbb04b"}, - {file = "multidict-6.2.0-cp312-cp312-win32.whl", hash = "sha256:76b34c12b013d813e6cb325e6bd4f9c984db27758b16085926bbe7ceeaace626"}, - {file = "multidict-6.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:0b183a959fb88ad1be201de2c4bdf52fa8e46e6c185d76201286a97b6f5ee65c"}, - {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5c5e7d2e300d5cb3b2693b6d60d3e8c8e7dd4ebe27cd17c9cb57020cac0acb80"}, - {file = "multidict-6.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:256d431fe4583c5f1e0f2e9c4d9c22f3a04ae96009b8cfa096da3a8723db0a16"}, - {file = "multidict-6.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a3c0ff89fe40a152e77b191b83282c9664357dce3004032d42e68c514ceff27e"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef7d48207926edbf8b16b336f779c557dd8f5a33035a85db9c4b0febb0706817"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c099d3899b14e1ce52262eb82a5f5cb92157bb5106bf627b618c090a0eadc"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e16e7297f29a544f49340012d6fc08cf14de0ab361c9eb7529f6a57a30cbfda1"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:042028348dc5a1f2be6c666437042a98a5d24cee50380f4c0902215e5ec41844"}, - {file = "multidict-6.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08549895e6a799bd551cf276f6e59820aa084f0f90665c0f03dd3a50db5d3c48"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ccfd74957ef53fa7380aaa1c961f523d582cd5e85a620880ffabd407f8202c0"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83b78c680d4b15d33042d330c2fa31813ca3974197bddb3836a5c635a5fd013f"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b4c153863dd6569f6511845922c53e39c8d61f6e81f228ad5443e690fca403de"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:98aa8325c7f47183b45588af9c434533196e241be0a4e4ae2190b06d17675c02"}, - {file = "multidict-6.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e658d1373c424457ddf6d55ec1db93c280b8579276bebd1f72f113072df8a5d"}, - {file = "multidict-6.2.0-cp313-cp313-win32.whl", hash = "sha256:3157126b028c074951839233647bd0e30df77ef1fedd801b48bdcad242a60f4e"}, - {file = "multidict-6.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:2e87f1926e91855ae61769ba3e3f7315120788c099677e0842e697b0bfb659f2"}, - {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:2529ddbdaa424b2c6c2eb668ea684dd6b75b839d0ad4b21aad60c168269478d7"}, - {file = "multidict-6.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:13551d0e2d7201f0959725a6a769b6f7b9019a168ed96006479c9ac33fe4096b"}, - {file = "multidict-6.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d1996ee1330e245cd3aeda0887b4409e3930524c27642b046e4fae88ffa66c5e"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c537da54ce4ff7c15e78ab1292e5799d0d43a2108e006578a57f531866f64025"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f249badb360b0b4d694307ad40f811f83df4da8cef7b68e429e4eea939e49dd"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48d39b1824b8d6ea7de878ef6226efbe0773f9c64333e1125e0efcfdd18a24c7"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b99aac6bb2c37db336fa03a39b40ed4ef2818bf2dfb9441458165ebe88b793af"}, - {file = "multidict-6.2.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bfa8bc649783e703263f783f73e27fef8cd37baaad4389816cf6a133141331"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2c00ad31fbc2cbac85d7d0fcf90853b2ca2e69d825a2d3f3edb842ef1544a2c"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d57a01a2a9fa00234aace434d8c131f0ac6e0ac6ef131eda5962d7e79edfb5b"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:abf5b17bc0cf626a8a497d89ac691308dbd825d2ac372aa990b1ca114e470151"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:f7716f7e7138252d88607228ce40be22660d6608d20fd365d596e7ca0738e019"}, - {file = "multidict-6.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a36953389f35f0a4e88dc796048829a2f467c9197265504593f0e420571547"}, - {file = "multidict-6.2.0-cp313-cp313t-win32.whl", hash = "sha256:e653d36b1bf48fa78c7fcebb5fa679342e025121ace8c87ab05c1cefd33b34fc"}, - {file = "multidict-6.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ca23db5fb195b5ef4fd1f77ce26cadefdf13dba71dab14dadd29b34d457d7c44"}, - {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b4f3d66dd0354b79761481fc15bdafaba0b9d9076f1f42cc9ce10d7fcbda205a"}, - {file = "multidict-6.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e2a2d6749e1ff2c9c76a72c6530d5baa601205b14e441e6d98011000f47a7ac"}, - {file = "multidict-6.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cca83a629f77402cfadd58352e394d79a61c8015f1694b83ab72237ec3941f88"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781b5dd1db18c9e9eacc419027b0acb5073bdec9de1675c0be25ceb10e2ad133"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf8d370b2fea27fb300825ec3984334f7dd54a581bde6456799ba3776915a656"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25bb96338512e2f46f615a2bb7c6012fe92a4a5ebd353e5020836a7e33120349"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e2819b0b468174de25c0ceed766606a07cedeab132383f1e83b9a4e96ccb4f"}, - {file = "multidict-6.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aed763b6a1b28c46c055692836879328f0b334a6d61572ee4113a5d0c859872"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a1133414b771619aa3c3000701c11b2e4624a7f492f12f256aedde97c28331a2"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:639556758c36093b35e2e368ca485dada6afc2bd6a1b1207d85ea6dfc3deab27"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:163f4604e76639f728d127293d24c3e208b445b463168af3d031b92b0998bb90"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2325105e16d434749e1be8022f942876a936f9bece4ec41ae244e3d7fae42aaf"}, - {file = "multidict-6.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e4371591e621579cb6da8401e4ea405b33ff25a755874a3567c4075ca63d56e2"}, - {file = "multidict-6.2.0-cp39-cp39-win32.whl", hash = "sha256:d1175b0e0d6037fab207f05774a176d71210ebd40b1c51f480a04b65ec5c786d"}, - {file = "multidict-6.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad81012b24b88aad4c70b2cbc2dad84018783221b7f923e926f4690ff8569da3"}, - {file = "multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530"}, - {file = "multidict-6.2.0.tar.gz", hash = "sha256:0085b0afb2446e57050140240a8595846ed64d1cbd26cef936bfab3192c673b8"}, + {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8b3dc0eec9304fa04d84a51ea13b0ec170bace5b7ddeaac748149efd316f1504"}, + {file = "multidict-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9534f3d84addd3b6018fa83f97c9d4247aaa94ac917d1ed7b2523306f99f5c16"}, + {file = "multidict-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a003ce1413ae01f0b8789c1c987991346a94620a4d22210f7a8fe753646d3209"}, + {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b43f7384e68b1b982c99f489921a459467b5584bdb963b25e0df57c9039d0ad"}, + {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d142ae84047262dc75c1f92eaf95b20680f85ce11d35571b4c97e267f96fadc4"}, + {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec7e86fbc48aa1d6d686501a8547818ba8d645e7e40eaa98232a5d43ee4380ad"}, + {file = "multidict-6.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe019fb437632b016e6cac67a7e964f1ef827ef4023f1ca0227b54be354da97e"}, + {file = "multidict-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b60cb81214a9da7cfd8ae2853d5e6e47225ece55fe5833142fe0af321c35299"}, + {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32d9e8ef2e0312d4e96ca9adc88e0675b6d8e144349efce4a7c95d5ccb6d88e0"}, + {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:335d584312e3fa43633d63175dfc1a5f137dd7aa03d38d1310237d54c3032774"}, + {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8df917faa6b8cac3d6870fc21cb7e4d169faca68e43ffe568c156c9c6408a4d"}, + {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc060b9b89b701dd8fedef5b99e1f1002b8cb95072693233a63389d37e48212d"}, + {file = "multidict-6.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f2ce3be2500658f3c644494b934628bb0c82e549dde250d2119689ce791cc8b8"}, + {file = "multidict-6.3.2-cp310-cp310-win32.whl", hash = "sha256:dbcb4490d8e74b484449abd51751b8f560dd0a4812eb5dacc6a588498222a9ab"}, + {file = "multidict-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:06944f9ced30f8602be873563ed4df7e3f40958f60b2db39732c11d615a33687"}, + {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45a034f41fcd16968c0470d8912d293d7b0d0822fc25739c5c2ff7835b85bc56"}, + {file = "multidict-6.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:352585cec45f5d83d886fc522955492bb436fca032b11d487b12d31c5a81b9e3"}, + {file = "multidict-6.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:da9d89d293511fd0a83a90559dc131f8b3292b6975eb80feff19e5f4663647e2"}, + {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79fa716592224aa652b9347a586cfe018635229074565663894eb4eb21f8307f"}, + {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0326278a44c56e94792475268e5cd3d47fbc0bd41ee56928c3bbb103ba7f58fe"}, + {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb1ea87f7fe45e5079f6315e95d64d4ca8b43ef656d98bed63a02e3756853a22"}, + {file = "multidict-6.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cff3c5a98d037024a9065aafc621a8599fad7b423393685dc83cf7a32f8b691"}, + {file = "multidict-6.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed99834b053c655d980fb98029003cb24281e47a796052faad4543aa9e01b8e8"}, + {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7048440e505d2b4741e5d0b32bd2f427c901f38c7760fc245918be2cf69b3b85"}, + {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27248c27b563f5889556da8a96e18e98a56ff807ac1a7d56cf4453c2c9e4cd91"}, + {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6323b4ba0e018bd266f776c35f3f0943fc4ee77e481593c9f93bd49888f24e94"}, + {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:81f7ce5ec7c27d0b45c10449c8f0fed192b93251e2e98cb0b21fec779ef1dc4d"}, + {file = "multidict-6.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03bfcf2825b3bed0ba08a9d854acd18b938cab0d2dba3372b51c78e496bac811"}, + {file = "multidict-6.3.2-cp311-cp311-win32.whl", hash = "sha256:f32c2790512cae6ca886920e58cdc8c784bdc4bb2a5ec74127c71980369d18dc"}, + {file = "multidict-6.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b0c15e58e038a2cd75ef7cf7e072bc39b5e0488b165902efb27978984bbad70"}, + {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d1e0ba1ce1b8cc79117196642d95f4365e118eaf5fb85f57cdbcc5a25640b2a4"}, + {file = "multidict-6.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:029bbd7d782251a78975214b78ee632672310f9233d49531fc93e8e99154af25"}, + {file = "multidict-6.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7db41e3b56817d9175264e5fe00192fbcb8e1265307a59f53dede86161b150e"}, + {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcab18e65cc555ac29981a581518c23311f2b1e72d8f658f9891590465383be"}, + {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d50eff89aa4d145a5486b171a2177042d08ea5105f813027eb1050abe91839f"}, + {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:643e57b403d3e240045a3681f9e6a04d35a33eddc501b4cbbbdbc9c70122e7bc"}, + {file = "multidict-6.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d17b37b9715b30605b5bab1460569742d0c309e5c20079263b440f5d7746e7e"}, + {file = "multidict-6.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:68acd51fa94e63312b8ddf84bfc9c3d3442fe1f9988bbe1b6c703043af8867fe"}, + {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:347eea2852ab7f697cc5ed9b1aae96b08f8529cca0c6468f747f0781b1842898"}, + {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4d3f8e57027dcda84a1aa181501c15c45eab9566eb6fcc274cbd1e7561224f8"}, + {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9ca57a841ffcf712e47875d026aa49d6e67f9560624d54b51628603700d5d287"}, + {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7cafdafb44c4e646118410368307693e49d19167e5f119cbe3a88697d2d1a636"}, + {file = "multidict-6.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:430120c6ce3715a9c6075cabcee557daccbcca8ba25a9fedf05c7bf564532f2d"}, + {file = "multidict-6.3.2-cp312-cp312-win32.whl", hash = "sha256:13bec31375235a68457ab887ce1bbf4f59d5810d838ae5d7e5b416242e1f3ed4"}, + {file = "multidict-6.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:c3b6d7620e6e90c6d97eaf3a63bf7fbd2ba253aab89120a4a9c660bf2d675391"}, + {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b9ca24700322816ae0d426aa33671cf68242f8cc85cee0d0e936465ddaee90b5"}, + {file = "multidict-6.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d9fbbe23667d596ff4f9f74d44b06e40ebb0ab6b262cf14a284f859a66f86457"}, + {file = "multidict-6.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cb602c5bea0589570ad3a4a6f2649c4f13cc7a1e97b4c616e5e9ff8dc490987"}, + {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93ca81dd4d1542e20000ed90f4cc84b7713776f620d04c2b75b8efbe61106c99"}, + {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18b6310b5454c62242577a128c87df8897f39dd913311cf2e1298e47dfc089eb"}, + {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a6dda57de1fc9aedfdb600a8640c99385cdab59a5716cb714b52b6005797f77"}, + {file = "multidict-6.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d8ec42d03cc6b29845552a68151f9e623c541f1708328353220af571e24a247"}, + {file = "multidict-6.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80681969cee2fa84dafeb53615d51d24246849984e3e87fbe4fe39956f2e23bf"}, + {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:01489b0c3592bb9d238e5690e9566db7f77a5380f054b57077d2c4deeaade0eb"}, + {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:522d9f1fd995d04dfedc0a40bca7e2591bc577d920079df50b56245a4a252c1c"}, + {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2014e9cf0b4e9c75bbad49c1758e5a9bf967a56184fc5fcc51527425baf5abba"}, + {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:78ced9fcbee79e446ff4bb3018ac7ba1670703de7873d9c1f6f9883db53c71bc"}, + {file = "multidict-6.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1faf01af972bd01216a107c195f5294f9f393531bc3e4faddc9b333581255d4d"}, + {file = "multidict-6.3.2-cp313-cp313-win32.whl", hash = "sha256:7a699ab13d8d8e1f885de1535b4f477fb93836c87168318244c2685da7b7f655"}, + {file = "multidict-6.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:8666bb0d883310c83be01676e302587834dfd185b52758caeab32ef0eb387bc6"}, + {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:d82c95aabee29612b1c4f48b98be98181686eb7d6c0152301f72715705cc787b"}, + {file = "multidict-6.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f47709173ea9e87a7fd05cd7e5cf1e5d4158924ff988a9a8e0fbd853705f0e68"}, + {file = "multidict-6.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c7f9d0276ceaab41b8ae78534ff28ea33d5de85db551cbf80c44371f2b55d13"}, + {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6eab22df44a25acab2e738f882f5ec551282ab45b2bbda5301e6d2cfb323036"}, + {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a947cb7c657f57874021b9b70c7aac049c877fb576955a40afa8df71d01a1390"}, + {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5faa346e8e1c371187cf345ab1e02a75889f9f510c9cbc575c31b779f7df084d"}, + {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc6e08d977aebf1718540533b4ba5b351ccec2db093370958a653b1f7f9219cc"}, + {file = "multidict-6.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98eab7acf55275b5bf09834125fa3a80b143a9f241cdcdd3f1295ffdc3c6d097"}, + {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:36863655630becc224375c0b99364978a0f95aebfb27fb6dd500f7fb5fb36e79"}, + {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d9c0979c096c0d46a963331b0e400d3a9e560e41219df4b35f0d7a2f28f39710"}, + {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0efc04f70f05e70e5945890767e8874da5953a196f5b07c552d305afae0f3bf6"}, + {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:2c519b3b82c34539fae3e22e4ea965869ac6b628794b1eb487780dde37637ab7"}, + {file = "multidict-6.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:329160e301f2afd7b43725d3dda8a7ef8ee41d4ceac2083fc0d8c1cc8a4bd56b"}, + {file = "multidict-6.3.2-cp313-cp313t-win32.whl", hash = "sha256:420e5144a5f598dad8db3128f1695cd42a38a0026c2991091dab91697832f8cc"}, + {file = "multidict-6.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:875faded2861c7af2682c67088e6313fec35ede811e071c96d36b081873cea14"}, + {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2516c5eb5732d6c4e29fa93323bfdc55186895124bc569e2404e3820934be378"}, + {file = "multidict-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:be5c8622e665cc5491c13c0fcd52915cdbae991a3514251d71129691338cdfb2"}, + {file = "multidict-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ef33150eea7953cfdb571d862cff894e0ad97ab80d97731eb4b9328fc32d52b"}, + {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40b357738ce46e998f1b1bad9c4b79b2a9755915f71b87a8c01ce123a22a4f99"}, + {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c60e059fcd3655a653ba99fec2556cd0260ec57f9cb138d3e6ffc413638a2e"}, + {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:629e7c5e75bde83e54a22c7043ce89d68691d1f103be6d09a1c82b870df3b4b8"}, + {file = "multidict-6.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6c8fc97d893fdf1fff15a619fee8de2f31c9b289ef7594730e35074fa0cefb"}, + {file = "multidict-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52081d2f27e0652265d4637b03f09b82f6da5ce5e1474f07dc64674ff8bfc04c"}, + {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:64529dc395b5fd0a7826ffa70d2d9a7f4abd8f5333d6aaaba67fdf7bedde9f21"}, + {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2b7c3fad827770840f5399348c89635ed6d6e9bba363baad7d3c7f86a9cf1da3"}, + {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:24aa42b1651c654ae9e5273e06c3b7ccffe9f7cc76fbde40c37e9ae65f170818"}, + {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:04ceea01e9991357164b12882e120ce6b4d63a0424bb9f9cd37910aa56d30830"}, + {file = "multidict-6.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:943897a41160945416617db567d867ab34e9258adaffc56a25a4c3f99d919598"}, + {file = "multidict-6.3.2-cp39-cp39-win32.whl", hash = "sha256:76157a9a0c5380aadd3b5ff7b8deee355ff5adecc66c837b444fa633b4d409a2"}, + {file = "multidict-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:d091d123e44035cd5664554308477aff0b58db37e701e7598a67e907b98d1925"}, + {file = "multidict-6.3.2-py3-none-any.whl", hash = "sha256:71409d4579f716217f23be2f5e7afca5ca926aaeb398aa11b72d793bff637a1f"}, + {file = "multidict-6.3.2.tar.gz", hash = "sha256:c1035eea471f759fa853dd6e76aaa1e389f93b3e1403093fa0fd3ab4db490678"}, ] [package.dependencies] @@ -2225,109 +2226,109 @@ virtualenv = ">=20.10.0" [[package]] name = "propcache" -version = "0.3.0" +version = "0.3.1" description = "Accelerated property cache" optional = false python-versions = ">=3.9" files = [ - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:efa44f64c37cc30c9f05932c740a8b40ce359f51882c70883cc95feac842da4d"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2383a17385d9800b6eb5855c2f05ee550f803878f344f58b6e194de08b96352c"}, - {file = "propcache-0.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e7420211f5a65a54675fd860ea04173cde60a7cc20ccfbafcccd155225f8bc"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3302c5287e504d23bb0e64d2a921d1eb4a03fb93a0a0aa3b53de059f5a5d737d"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e2e068a83552ddf7a39a99488bcba05ac13454fb205c847674da0352602082f"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d913d36bdaf368637b4f88d554fb9cb9d53d6920b9c5563846555938d5450bf"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ee1983728964d6070ab443399c476de93d5d741f71e8f6e7880a065f878e0b9"}, - {file = "propcache-0.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36ca5e9a21822cc1746023e88f5c0af6fce3af3b85d4520efb1ce4221bed75cc"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9ecde3671e62eeb99e977f5221abcf40c208f69b5eb986b061ccec317c82ebd0"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d383bf5e045d7f9d239b38e6acadd7b7fdf6c0087259a84ae3475d18e9a2ae8b"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8cb625bcb5add899cb8ba7bf716ec1d3e8f7cdea9b0713fa99eadf73b6d4986f"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5fa159dcee5dba00c1def3231c249cf261185189205073bde13797e57dd7540a"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a7080b0159ce05f179cfac592cda1a82898ca9cd097dacf8ea20ae33474fbb25"}, - {file = "propcache-0.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ed7161bccab7696a473fe7ddb619c1d75963732b37da4618ba12e60899fefe4f"}, - {file = "propcache-0.3.0-cp310-cp310-win32.whl", hash = "sha256:bf0d9a171908f32d54f651648c7290397b8792f4303821c42a74e7805bfb813c"}, - {file = "propcache-0.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:42924dc0c9d73e49908e35bbdec87adedd651ea24c53c29cac103ede0ea1d340"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9ddd49258610499aab83b4f5b61b32e11fce873586282a0e972e5ab3bcadee51"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2578541776769b500bada3f8a4eeaf944530516b6e90c089aa368266ed70c49e"}, - {file = "propcache-0.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8074c5dd61c8a3e915fa8fc04754fa55cfa5978200d2daa1e2d4294c1f136aa"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b58229a844931bca61b3a20efd2be2a2acb4ad1622fc026504309a6883686fbf"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e45377d5d6fefe1677da2a2c07b024a6dac782088e37c0b1efea4cfe2b1be19b"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ec5060592d83454e8063e487696ac3783cc48c9a329498bafae0d972bc7816c9"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15010f29fbed80e711db272909a074dc79858c6d28e2915704cfc487a8ac89c6"}, - {file = "propcache-0.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a254537b9b696ede293bfdbc0a65200e8e4507bc9f37831e2a0318a9b333c85c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2b975528998de037dfbc10144b8aed9b8dd5a99ec547f14d1cb7c5665a43f075"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:19d36bb351ad5554ff20f2ae75f88ce205b0748c38b146c75628577020351e3c"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6032231d4a5abd67c7f71168fd64a47b6b451fbcb91c8397c2f7610e67683810"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6985a593417cdbc94c7f9c3403747335e450c1599da1647a5af76539672464d3"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a1948df1bb1d56b5e7b0553c0fa04fd0e320997ae99689488201f19fa90d2e7"}, - {file = "propcache-0.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8319293e85feadbbfe2150a5659dbc2ebc4afdeaf7d98936fb9a2f2ba0d4c35c"}, - {file = "propcache-0.3.0-cp311-cp311-win32.whl", hash = "sha256:63f26258a163c34542c24808f03d734b338da66ba91f410a703e505c8485791d"}, - {file = "propcache-0.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:cacea77ef7a2195f04f9279297684955e3d1ae4241092ff0cfcef532bb7a1c32"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e53d19c2bf7d0d1e6998a7e693c7e87300dd971808e6618964621ccd0e01fe4e"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a61a68d630e812b67b5bf097ab84e2cd79b48c792857dc10ba8a223f5b06a2af"}, - {file = "propcache-0.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fb91d20fa2d3b13deea98a690534697742029f4fb83673a3501ae6e3746508b5"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67054e47c01b7b349b94ed0840ccae075449503cf1fdd0a1fdd98ab5ddc2667b"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997e7b8f173a391987df40f3b52c423e5850be6f6df0dcfb5376365440b56667"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d663fd71491dde7dfdfc899d13a067a94198e90695b4321084c6e450743b8c7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8884ba1a0fe7210b775106b25850f5e5a9dc3c840d1ae9924ee6ea2eb3acbfe7"}, - {file = "propcache-0.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa806bbc13eac1ab6291ed21ecd2dd426063ca5417dd507e6be58de20e58dfcf"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f4d7a7c0aff92e8354cceca6fe223973ddf08401047920df0fcb24be2bd5138"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9be90eebc9842a93ef8335291f57b3b7488ac24f70df96a6034a13cb58e6ff86"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bf15fc0b45914d9d1b706f7c9c4f66f2b7b053e9517e40123e137e8ca8958b3d"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a16167118677d94bb48bfcd91e420088854eb0737b76ec374b91498fb77a70e"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:41de3da5458edd5678b0f6ff66691507f9885f5fe6a0fb99a5d10d10c0fd2d64"}, - {file = "propcache-0.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:728af36011bb5d344c4fe4af79cfe186729efb649d2f8b395d1572fb088a996c"}, - {file = "propcache-0.3.0-cp312-cp312-win32.whl", hash = "sha256:6b5b7fd6ee7b54e01759f2044f936dcf7dea6e7585f35490f7ca0420fe723c0d"}, - {file = "propcache-0.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:2d15bc27163cd4df433e75f546b9ac31c1ba7b0b128bfb1b90df19082466ff57"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a2b9bf8c79b660d0ca1ad95e587818c30ccdb11f787657458d6f26a1ea18c568"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0c1a133d42c6fc1f5fbcf5c91331657a1ff822e87989bf4a6e2e39b818d0ee9"}, - {file = "propcache-0.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb2f144c6d98bb5cbc94adeb0447cfd4c0f991341baa68eee3f3b0c9c0e83767"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1323cd04d6e92150bcc79d0174ce347ed4b349d748b9358fd2e497b121e03c8"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b812b3cb6caacd072276ac0492d249f210006c57726b6484a1e1805b3cfeea0"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:742840d1d0438eb7ea4280f3347598f507a199a35a08294afdcc560c3739989d"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c6e7e4f9167fddc438cd653d826f2222222564daed4116a02a184b464d3ef05"}, - {file = "propcache-0.3.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a94ffc66738da99232ddffcf7910e0f69e2bbe3a0802e54426dbf0714e1c2ffe"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c6ec957025bf32b15cbc6b67afe233c65b30005e4c55fe5768e4bb518d712f1"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:549722908de62aa0b47a78b90531c022fa6e139f9166be634f667ff45632cc92"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5d62c4f6706bff5d8a52fd51fec6069bef69e7202ed481486c0bc3874912c787"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:24c04f8fbf60094c531667b8207acbae54146661657a1b1be6d3ca7773b7a545"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7c5f5290799a3f6539cc5e6f474c3e5c5fbeba74a5e1e5be75587746a940d51e"}, - {file = "propcache-0.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0e7c9c3cf7c276d4f6ab9af8adddc127d04e0fcabede315904d2ff76db626"}, - {file = "propcache-0.3.0-cp313-cp313-win32.whl", hash = "sha256:ee0bd3a7b2e184e88d25c9baa6a9dc609ba25b76daae942edfb14499ac7ec374"}, - {file = "propcache-0.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f7d896a16da9455f882870a507567d4f58c53504dc2d4b1e1d386dfe4588a"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e560fd75aaf3e5693b91bcaddd8b314f4d57e99aef8a6c6dc692f935cc1e6bbf"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65a37714b8ad9aba5780325228598a5b16c47ba0f8aeb3dc0514701e4413d7c0"}, - {file = "propcache-0.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:07700939b2cbd67bfb3b76a12e1412405d71019df00ca5697ce75e5ef789d829"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c0fdbdf6983526e269e5a8d53b7ae3622dd6998468821d660d0daf72779aefa"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:794c3dd744fad478b6232289c866c25406ecdfc47e294618bdf1697e69bd64a6"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4544699674faf66fb6b4473a1518ae4999c1b614f0b8297b1cef96bac25381db"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddb8870bdb83456a489ab67c6b3040a8d5a55069aa6f72f9d872235fbc52f54"}, - {file = "propcache-0.3.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f857034dc68d5ceb30fb60afb6ff2103087aea10a01b613985610e007053a121"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:02df07041e0820cacc8f739510078f2aadcfd3fc57eaeeb16d5ded85c872c89e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f47d52fd9b2ac418c4890aad2f6d21a6b96183c98021f0a48497a904199f006e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9ff4e9ecb6e4b363430edf2c6e50173a63e0820e549918adef70515f87ced19a"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ecc2920630283e0783c22e2ac94427f8cca29a04cfdf331467d4f661f4072dac"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c441c841e82c5ba7a85ad25986014be8d7849c3cfbdb6004541873505929a74e"}, - {file = "propcache-0.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c929916cbdb540d3407c66f19f73387f43e7c12fa318a66f64ac99da601bcdf"}, - {file = "propcache-0.3.0-cp313-cp313t-win32.whl", hash = "sha256:0c3e893c4464ebd751b44ae76c12c5f5c1e4f6cbd6fbf67e3783cd93ad221863"}, - {file = "propcache-0.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:75e872573220d1ee2305b35c9813626e620768248425f58798413e9c39741f46"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:03c091bb752349402f23ee43bb2bff6bd80ccab7c9df6b88ad4322258d6960fc"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46ed02532cb66612d42ae5c3929b5e98ae330ea0f3900bc66ec5f4862069519b"}, - {file = "propcache-0.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11ae6a8a01b8a4dc79093b5d3ca2c8a4436f5ee251a9840d7790dccbd96cb649"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df03cd88f95b1b99052b52b1bb92173229d7a674df0ab06d2b25765ee8404bce"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03acd9ff19021bd0567582ac88f821b66883e158274183b9e5586f678984f8fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd54895e4ae7d32f1e3dd91261df46ee7483a735017dc6f987904f194aa5fd14"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a67e5c04e3119594d8cfae517f4b9330c395df07ea65eab16f3d559b7068fe"}, - {file = "propcache-0.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee25f1ac091def37c4b59d192bbe3a206298feeb89132a470325bf76ad122a1e"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58e6d2a5a7cb3e5f166fd58e71e9a4ff504be9dc61b88167e75f835da5764d07"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:be90c94570840939fecedf99fa72839aed70b0ced449b415c85e01ae67422c90"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49ea05212a529c2caffe411e25a59308b07d6e10bf2505d77da72891f9a05641"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:119e244ab40f70a98c91906d4c1f4c5f2e68bd0b14e7ab0a06922038fae8a20f"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:507c5357a8d8b4593b97fb669c50598f4e6cccbbf77e22fa9598aba78292b4d7"}, - {file = "propcache-0.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8526b0941ec5a40220fc4dfde76aed58808e2b309c03e9fa8e2260083ef7157f"}, - {file = "propcache-0.3.0-cp39-cp39-win32.whl", hash = "sha256:7cedd25e5f678f7738da38037435b340694ab34d424938041aa630d8bac42663"}, - {file = "propcache-0.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:bf4298f366ca7e1ad1d21bbb58300a6985015909964077afd37559084590c929"}, - {file = "propcache-0.3.0-py3-none-any.whl", hash = "sha256:67dda3c7325691c2081510e92c561f465ba61b975f481735aefdfc845d2cd043"}, - {file = "propcache-0.3.0.tar.gz", hash = "sha256:a8fd93de4e1d278046345f49e2238cdb298589325849b2645d4a94c53faeffc5"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f27785888d2fdd918bc36de8b8739f2d6c791399552333721b58193f68ea3e98"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4e89cde74154c7b5957f87a355bb9c8ec929c167b59c83d90654ea36aeb6180"}, + {file = "propcache-0.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:730178f476ef03d3d4d255f0c9fa186cb1d13fd33ffe89d39f2cda4da90ceb71"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:967a8eec513dbe08330f10137eacb427b2ca52118769e82ebcfcab0fba92a649"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b9145c35cc87313b5fd480144f8078716007656093d23059e8993d3a8fa730f"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e64e948ab41411958670f1093c0a57acfdc3bee5cf5b935671bbd5313bcf229"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319fa8765bfd6a265e5fa661547556da381e53274bc05094fc9ea50da51bfd46"}, + {file = "propcache-0.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66d8ccbc902ad548312b96ed8d5d266d0d2c6d006fd0f66323e9d8f2dd49be7"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2d219b0dbabe75e15e581fc1ae796109b07c8ba7d25b9ae8d650da582bed01b0"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd6a55f65241c551eb53f8cf4d2f4af33512c39da5d9777694e9d9c60872f519"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9979643ffc69b799d50d3a7b72b5164a2e97e117009d7af6dfdd2ab906cb72cd"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4cf9e93a81979f1424f1a3d155213dc928f1069d697e4353edb8a5eba67c6259"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2fce1df66915909ff6c824bbb5eb403d2d15f98f1518e583074671a30fe0c21e"}, + {file = "propcache-0.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4d0dfdd9a2ebc77b869a0b04423591ea8823f791293b527dc1bb896c1d6f1136"}, + {file = "propcache-0.3.1-cp310-cp310-win32.whl", hash = "sha256:1f6cc0ad7b4560e5637eb2c994e97b4fa41ba8226069c9277eb5ea7101845b42"}, + {file = "propcache-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:47ef24aa6511e388e9894ec16f0fbf3313a53ee68402bc428744a367ec55b833"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7f30241577d2fef2602113b70ef7231bf4c69a97e04693bde08ddab913ba0ce5"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:43593c6772aa12abc3af7784bff4a41ffa921608dd38b77cf1dfd7f5c4e71371"}, + {file = "propcache-0.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a75801768bbe65499495660b777e018cbe90c7980f07f8aa57d6be79ea6f71da"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6f1324db48f001c2ca26a25fa25af60711e09b9aaf4b28488602776f4f9a744"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cdb0f3e1eb6dfc9965d19734d8f9c481b294b5274337a8cb5cb01b462dcb7e0"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1eb34d90aac9bfbced9a58b266f8946cb5935869ff01b164573a7634d39fbcb5"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35c7070eeec2cdaac6fd3fe245226ed2a6292d3ee8c938e5bb645b434c5f256"}, + {file = "propcache-0.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b23c11c2c9e6d4e7300c92e022046ad09b91fd00e36e83c44483df4afa990073"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e19ea4ea0bf46179f8a3652ac1426e6dcbaf577ce4b4f65be581e237340420d"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:bd39c92e4c8f6cbf5f08257d6360123af72af9f4da75a690bef50da77362d25f"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0313e8b923b3814d1c4a524c93dfecea5f39fa95601f6a9b1ac96cd66f89ea0"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e861ad82892408487be144906a368ddbe2dc6297074ade2d892341b35c59844a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:61014615c1274df8da5991a1e5da85a3ccb00c2d4701ac6f3383afd3ca47ab0a"}, + {file = "propcache-0.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:71ebe3fe42656a2328ab08933d420df5f3ab121772eef78f2dc63624157f0ed9"}, + {file = "propcache-0.3.1-cp311-cp311-win32.whl", hash = "sha256:58aa11f4ca8b60113d4b8e32d37e7e78bd8af4d1a5b5cb4979ed856a45e62005"}, + {file = "propcache-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:9532ea0b26a401264b1365146c440a6d78269ed41f83f23818d4b79497aeabe7"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976"}, + {file = "propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25"}, + {file = "propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5"}, + {file = "propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7"}, + {file = "propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b"}, + {file = "propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f"}, + {file = "propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120"}, + {file = "propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7"}, + {file = "propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef"}, + {file = "propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24"}, + {file = "propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c"}, + {file = "propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458"}, + {file = "propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18"}, + {file = "propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a"}, + {file = "propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d"}, + {file = "propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ed5f6d2edbf349bd8d630e81f474d33d6ae5d07760c44d33cd808e2f5c8f4ae6"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:668ddddc9f3075af019f784456267eb504cb77c2c4bd46cc8402d723b4d200bf"}, + {file = "propcache-0.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0c86e7ceea56376216eba345aa1fc6a8a6b27ac236181f840d1d7e6a1ea9ba5c"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83be47aa4e35b87c106fc0c84c0fc069d3f9b9b06d3c494cd404ec6747544894"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:27c6ac6aa9fc7bc662f594ef380707494cb42c22786a558d95fcdedb9aa5d035"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a956dff37080b352c1c40b2966b09defb014347043e740d420ca1eb7c9b908"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82de5da8c8893056603ac2d6a89eb8b4df49abf1a7c19d536984c8dd63f481d5"}, + {file = "propcache-0.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c3c3a203c375b08fd06a20da3cf7aac293b834b6f4f4db71190e8422750cca5"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b303b194c2e6f171cfddf8b8ba30baefccf03d36a4d9cab7fd0bb68ba476a3d7"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:916cd229b0150129d645ec51614d38129ee74c03293a9f3f17537be0029a9641"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a461959ead5b38e2581998700b26346b78cd98540b5524796c175722f18b0294"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:069e7212890b0bcf9b2be0a03afb0c2d5161d91e1bf51569a64f629acc7defbf"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ef2e4e91fb3945769e14ce82ed53007195e616a63aa43b40fb7ebaaf907c8d4c"}, + {file = "propcache-0.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8638f99dca15b9dff328fb6273e09f03d1c50d9b6512f3b65a4154588a7595fe"}, + {file = "propcache-0.3.1-cp39-cp39-win32.whl", hash = "sha256:6f173bbfe976105aaa890b712d1759de339d8a7cef2fc0a1714cc1a1e1c47f64"}, + {file = "propcache-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:603f1fe4144420374f1a69b907494c3acbc867a581c2d49d4175b0de7cc64566"}, + {file = "propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40"}, + {file = "propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf"}, ] [[package]] @@ -2377,19 +2378,20 @@ files = [ [[package]] name = "pydantic" -version = "2.10.6" +version = "2.11.2" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, - {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, + {file = "pydantic-2.11.2-py3-none-any.whl", hash = "sha256:7f17d25846bcdf89b670a86cdfe7b29a9f1c9ca23dee154221c9aa81845cfca7"}, + {file = "pydantic-2.11.2.tar.gz", hash = "sha256:2138628e050bd7a1e70b91d4bf4a91167f4ad76fdb83209b107c8d84b854917e"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.2" +pydantic-core = "2.33.1" typing-extensions = ">=4.12.2" +typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -2397,111 +2399,110 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.33.1" description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, - {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, - {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, - {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, - {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, - {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, - {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, - {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, - {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, - {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, - {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, - {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, - {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, - {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, - {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, - {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, - {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, - {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, - {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, - {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, - {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, - {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, - {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, - {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, - {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, - {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, + {file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"}, + {file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89"}, + {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde"}, + {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65"}, + {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc"}, + {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091"}, + {file = "pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383"}, + {file = "pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504"}, + {file = "pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24"}, + {file = "pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f"}, + {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77"}, + {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961"}, + {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1"}, + {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c"}, + {file = "pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896"}, + {file = "pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83"}, + {file = "pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89"}, + {file = "pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8"}, + {file = "pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d"}, + {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b"}, + {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39"}, + {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a"}, + {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db"}, + {file = "pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda"}, + {file = "pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4"}, + {file = "pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea"}, + {file = "pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a"}, + {file = "pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d"}, + {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4"}, + {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde"}, + {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e"}, + {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd"}, + {file = "pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f"}, + {file = "pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40"}, + {file = "pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523"}, + {file = "pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d"}, + {file = "pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c"}, + {file = "pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18"}, + {file = "pydantic_core-2.33.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5ab77f45d33d264de66e1884fca158bc920cb5e27fd0764a72f72f5756ae8bdb"}, + {file = "pydantic_core-2.33.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7aaba1b4b03aaea7bb59e1b5856d734be011d3e6d98f5bcaa98cb30f375f2ad"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb66263e9ba8fea2aa85e1e5578980d127fb37d7f2e292773e7bc3a38fb0c7b"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f2648b9262607a7fb41d782cc263b48032ff7a03a835581abbf7a3bec62bcf5"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723c5630c4259400818b4ad096735a829074601805d07f8cafc366d95786d331"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d100e3ae783d2167782391e0c1c7a20a31f55f8015f3293647544df3f9c67824"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177d50460bc976a0369920b6c744d927b0ecb8606fb56858ff542560251b19e5"}, + {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3edde68d1a1f9af1273b2fe798997b33f90308fb6d44d8550c89fc6a3647cf6"}, + {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a62c3c3ef6a7e2c45f7853b10b5bc4ddefd6ee3cd31024754a1a5842da7d598d"}, + {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c91dbb0ab683fa0cd64a6e81907c8ff41d6497c346890e26b23de7ee55353f96"}, + {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f466e8bf0a62dc43e068c12166281c2eca72121dd2adc1040f3aa1e21ef8599"}, + {file = "pydantic_core-2.33.1-cp39-cp39-win32.whl", hash = "sha256:ab0277cedb698749caada82e5d099dc9fed3f906a30d4c382d1a21725777a1e5"}, + {file = "pydantic_core-2.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:5773da0ee2d17136b1f1c6fbde543398d452a6ad2a7b54ea1033e2daa739b8d2"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add"}, + {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544"}, + {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7edbc454a29fc6aeae1e1eecba4f07b63b8d76e76a748532233c4c167b4cb9ea"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad05b683963f69a1d5d2c2bdab1274a31221ca737dbbceaa32bcb67359453cdd"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6a94bf9452c6da9b5d76ed229a5683d0306ccb91cca8e1eea883189780d568"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7965c13b3967909a09ecc91f21d09cfc4576bf78140b988904e94f130f188396"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f1fdb790440a34f6ecf7679e1863b825cb5ffde858a9197f851168ed08371e5"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5277aec8d879f8d05168fdd17ae811dd313b8ff894aeeaf7cd34ad28b4d77e33"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8ab581d3530611897d863d1a649fb0644b860286b4718db919bfd51ece41f10b"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0483847fa9ad5e3412265c1bd72aad35235512d9ce9d27d81a56d935ef489672"}, + {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3"}, + {file = "pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df"}, ] [package.dependencies] @@ -2599,13 +2600,13 @@ files = [ [[package]] name = "pyparsing" -version = "3.2.2" +version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" files = [ - {file = "pyparsing-3.2.2-py3-none-any.whl", hash = "sha256:6ab05e1cb111cc72acc8ed811a3ca4c2be2af8d7b6df324347f04fd057d8d793"}, - {file = "pyparsing-3.2.2.tar.gz", hash = "sha256:2a857aee851f113c2de9d4bfd9061baea478cb0f1c7ca6cbf594942d6d111575"}, + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, ] [package.extras] @@ -2653,13 +2654,13 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "6.1.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" files = [ - {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, - {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, + {file = "pytest_cov-6.1.0-py3-none-any.whl", hash = "sha256:cd7e1d54981d5185ef2b8d64b50172ce97e6f357e6df5cb103e828c7f993e201"}, + {file = "pytest_cov-6.1.0.tar.gz", hash = "sha256:ec55e828c66755e5b74a21bd7cc03c303a9f928389c0563e50ba454a6dbe71db"}, ] [package.dependencies] @@ -2685,13 +2686,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.1.0" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, + {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, ] [package.extras] @@ -2699,13 +2700,13 @@ cli = ["click (>=5.0)"] [[package]] name = "pytz" -version = "2025.1" +version = "2025.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, - {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] [[package]] @@ -2809,114 +2810,125 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rpds-py" -version = "0.23.1" +version = "0.24.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" files = [ - {file = "rpds_py-0.23.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2a54027554ce9b129fc3d633c92fa33b30de9f08bc61b32c053dc9b537266fed"}, - {file = "rpds_py-0.23.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5ef909a37e9738d146519657a1aab4584018746a18f71c692f2f22168ece40c"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ee9d6f0b38efb22ad94c3b68ffebe4c47865cdf4b17f6806d6c674e1feb4246"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7356a6da0562190558c4fcc14f0281db191cdf4cb96e7604c06acfcee96df15"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9441af1d25aed96901f97ad83d5c3e35e6cd21a25ca5e4916c82d7dd0490a4fa"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d8abf7896a91fb97e7977d1aadfcc2c80415d6dc2f1d0fca5b8d0df247248f3"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b08027489ba8fedde72ddd233a5ea411b85a6ed78175f40285bd401bde7466d"}, - {file = "rpds_py-0.23.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fee513135b5a58f3bb6d89e48326cd5aa308e4bcdf2f7d59f67c861ada482bf8"}, - {file = "rpds_py-0.23.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:35d5631ce0af26318dba0ae0ac941c534453e42f569011585cb323b7774502a5"}, - {file = "rpds_py-0.23.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a20cb698c4a59c534c6701b1c24a968ff2768b18ea2991f886bd8985ce17a89f"}, - {file = "rpds_py-0.23.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e9c206a1abc27e0588cf8b7c8246e51f1a16a103734f7750830a1ccb63f557a"}, - {file = "rpds_py-0.23.1-cp310-cp310-win32.whl", hash = "sha256:d9f75a06ecc68f159d5d7603b734e1ff6daa9497a929150f794013aa9f6e3f12"}, - {file = "rpds_py-0.23.1-cp310-cp310-win_amd64.whl", hash = "sha256:f35eff113ad430b5272bbfc18ba111c66ff525828f24898b4e146eb479a2cdda"}, - {file = "rpds_py-0.23.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b79f5ced71efd70414a9a80bbbfaa7160da307723166f09b69773153bf17c590"}, - {file = "rpds_py-0.23.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9e799dac1ffbe7b10c1fd42fe4cd51371a549c6e108249bde9cd1200e8f59b4"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721f9c4011b443b6e84505fc00cc7aadc9d1743f1c988e4c89353e19c4a968ee"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f88626e3f5e57432e6191cd0c5d6d6b319b635e70b40be2ffba713053e5147dd"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:285019078537949cecd0190f3690a0b0125ff743d6a53dfeb7a4e6787af154f5"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92f5654157de1379c509b15acec9d12ecf6e3bc1996571b6cb82a4302060447"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e768267cbe051dd8d1c5305ba690bb153204a09bf2e3de3ae530de955f5b5580"}, - {file = "rpds_py-0.23.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5334a71f7dc1160382d45997e29f2637c02f8a26af41073189d79b95d3321f1"}, - {file = "rpds_py-0.23.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6adb81564af0cd428910f83fa7da46ce9ad47c56c0b22b50872bc4515d91966"}, - {file = "rpds_py-0.23.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cafa48f2133d4daa028473ede7d81cd1b9f9e6925e9e4003ebdf77010ee02f35"}, - {file = "rpds_py-0.23.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fced9fd4a07a1ded1bac7e961ddd9753dd5d8b755ba8e05acba54a21f5f1522"}, - {file = "rpds_py-0.23.1-cp311-cp311-win32.whl", hash = "sha256:243241c95174b5fb7204c04595852fe3943cc41f47aa14c3828bc18cd9d3b2d6"}, - {file = "rpds_py-0.23.1-cp311-cp311-win_amd64.whl", hash = "sha256:11dd60b2ffddba85715d8a66bb39b95ddbe389ad2cfcf42c833f1bcde0878eaf"}, - {file = "rpds_py-0.23.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c"}, - {file = "rpds_py-0.23.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc"}, - {file = "rpds_py-0.23.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35"}, - {file = "rpds_py-0.23.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b"}, - {file = "rpds_py-0.23.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef"}, - {file = "rpds_py-0.23.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad"}, - {file = "rpds_py-0.23.1-cp312-cp312-win32.whl", hash = "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057"}, - {file = "rpds_py-0.23.1-cp312-cp312-win_amd64.whl", hash = "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165"}, - {file = "rpds_py-0.23.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935"}, - {file = "rpds_py-0.23.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013"}, - {file = "rpds_py-0.23.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64"}, - {file = "rpds_py-0.23.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8"}, - {file = "rpds_py-0.23.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957"}, - {file = "rpds_py-0.23.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93"}, - {file = "rpds_py-0.23.1-cp313-cp313-win32.whl", hash = "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd"}, - {file = "rpds_py-0.23.1-cp313-cp313-win_amd64.whl", hash = "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70"}, - {file = "rpds_py-0.23.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731"}, - {file = "rpds_py-0.23.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722"}, - {file = "rpds_py-0.23.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e"}, - {file = "rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6"}, - {file = "rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b"}, - {file = "rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5"}, - {file = "rpds_py-0.23.1-cp313-cp313t-win32.whl", hash = "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7"}, - {file = "rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d"}, - {file = "rpds_py-0.23.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:09cd7dbcb673eb60518231e02874df66ec1296c01a4fcd733875755c02014b19"}, - {file = "rpds_py-0.23.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c6760211eee3a76316cf328f5a8bd695b47b1626d21c8a27fb3b2473a884d597"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72e680c1518733b73c994361e4b06441b92e973ef7d9449feec72e8ee4f713da"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae28144c1daa61366205d32abd8c90372790ff79fc60c1a8ad7fd3c8553a600e"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c698d123ce5d8f2d0cd17f73336615f6a2e3bdcedac07a1291bb4d8e7d82a05a"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98b257ae1e83f81fb947a363a274c4eb66640212516becaff7bef09a5dceacaa"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c9ff044eb07c8468594d12602291c635da292308c8c619244e30698e7fc455a"}, - {file = "rpds_py-0.23.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7938c7b0599a05246d704b3f5e01be91a93b411d0d6cc62275f025293b8a11ce"}, - {file = "rpds_py-0.23.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e9cb79ecedfc156c0692257ac7ed415243b6c35dd969baa461a6888fc79f2f07"}, - {file = "rpds_py-0.23.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7b77e07233925bd33fc0022b8537774423e4c6680b6436316c5075e79b6384f4"}, - {file = "rpds_py-0.23.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a970bfaf130c29a679b1d0a6e0f867483cea455ab1535fb427566a475078f27f"}, - {file = "rpds_py-0.23.1-cp39-cp39-win32.whl", hash = "sha256:4233df01a250b3984465faed12ad472f035b7cd5240ea3f7c76b7a7016084495"}, - {file = "rpds_py-0.23.1-cp39-cp39-win_amd64.whl", hash = "sha256:c617d7453a80e29d9973b926983b1e700a9377dbe021faa36041c78537d7b08c"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c1f8afa346ccd59e4e5630d5abb67aba6a9812fddf764fd7eb11f382a345f8cc"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fad784a31869747df4ac968a351e070c06ca377549e4ace94775aaa3ab33ee06"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5a96fcac2f18e5a0a23a75cd27ce2656c66c11c127b0318e508aab436b77428"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e77febf227a1dc3220159355dba68faa13f8dca9335d97504abf428469fb18b"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26bb3e8de93443d55e2e748e9fd87deb5f8075ca7bc0502cfc8be8687d69a2ec"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db7707dde9143a67b8812c7e66aeb2d843fe33cc8e374170f4d2c50bd8f2472d"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eedaaccc9bb66581d4ae7c50e15856e335e57ef2734dbc5fd8ba3e2a4ab3cb6"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28358c54fffadf0ae893f6c1050e8f8853e45df22483b7fff2f6ab6152f5d8bf"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:633462ef7e61d839171bf206551d5ab42b30b71cac8f10a64a662536e057fdef"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a98f510d86f689fcb486dc59e6e363af04151e5260ad1bdddb5625c10f1e95f8"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e0397dd0b3955c61ef9b22838144aa4bef6f0796ba5cc8edfc64d468b93798b4"}, - {file = "rpds_py-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:75307599f0d25bf6937248e5ac4e3bde5ea72ae6618623b86146ccc7845ed00b"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3614d280bf7aab0d3721b5ce0e73434acb90a2c993121b6e81a1c15c665298ac"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e5963ea87f88bddf7edd59644a35a0feecf75f8985430124c253612d4f7d27ae"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad76f44f70aac3a54ceb1813ca630c53415da3a24fd93c570b2dfb4856591017"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c6ae11e6e93728d86aafc51ced98b1658a0080a7dd9417d24bfb955bb09c3c2"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc869af5cba24d45fb0399b0cfdbcefcf6910bf4dee5d74036a57cf5264b3ff4"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76b32eb2ab650a29e423525e84eb197c45504b1c1e6e17b6cc91fcfeb1a4b1d"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4263320ed887ed843f85beba67f8b2d1483b5947f2dc73a8b068924558bfeace"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7f9682a8f71acdf59fd554b82b1c12f517118ee72c0f3944eda461606dfe7eb9"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:754fba3084b70162a6b91efceee8a3f06b19e43dac3f71841662053c0584209a"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:a1c66e71ecfd2a4acf0e4bd75e7a3605afa8f9b28a3b497e4ba962719df2be57"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:8d67beb6002441faef8251c45e24994de32c4c8686f7356a1f601ad7c466f7c3"}, - {file = "rpds_py-0.23.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a1e17d8dc8e57d8e0fd21f8f0f0a5211b3fa258b2e444c2053471ef93fe25a00"}, - {file = "rpds_py-0.23.1.tar.gz", hash = "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707"}, + {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, + {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, + {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, + {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, + {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, + {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, + {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, + {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, + {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, + {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, + {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, + {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, + {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, + {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, + {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, + {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, + {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, + {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, + {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, + {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, + {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, + {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, + {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, + {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d"}, + {file = "rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c"}, + {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149"}, + {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45"}, + {file = "rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103"}, + {file = "rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, + {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, + {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796"}, + {file = "rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f"}, + {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, ] [[package]] @@ -3290,15 +3302,29 @@ files = [ [[package]] name = "typing-extensions" -version = "4.12.2" +version = "4.13.1" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.13.1-py3-none-any.whl", hash = "sha256:4b6cf02909eb5495cfbc3f6e8fd49217e6cc7944e145cdda8caa3734777f9e69"}, + {file = "typing_extensions-4.13.1.tar.gz", hash = "sha256:98795af00fb9640edec5b8e31fc647597b4691f099ad75f469a2616be1a76dff"}, ] +[[package]] +name = "typing-inspection" +version = "0.4.0" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +files = [ + {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, + {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + [[package]] name = "tzdata" version = "2025.2" @@ -3405,13 +3431,13 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", [[package]] name = "virtualenv" -version = "20.29.3" +version = "20.30.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ - {file = "virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170"}, - {file = "virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac"}, + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, ] [package.dependencies] diff --git a/api/pyproject.toml b/api/pyproject.toml index 6b2263f..41215d8 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -96,7 +96,7 @@ ignore = [ "./tests/v0/mockers/*.py" = ["C901"] # Allow high code complexity in mockers "./src/v0/database/client.py" = ["B024"] # Allow Abstract class without abstractmethod "./src/v0/services/base.py" = ["B024"] # Allow Abstract class without abstractmethod -"./src/services/classes/directed_graph.py" = ["B024"] # Allow Abstract class without abstractmethod +"./src/v0/services/classes/abstract_directed_graph.py" = ["B024"] # Allow Abstract class without abstractmethod [tool.codespell] skip = "*.lock" diff --git a/api/src/v0/models/structure.py b/api/src/v0/models/structure.py index 19cba77..543996f 100644 --- a/api/src/v0/models/structure.py +++ b/api/src/v0/models/structure.py @@ -124,7 +124,7 @@ class DecisionTreeResponse(DOTModel): "name": "Joe can test the car", "shortname": "Test", "uuid": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "alternatives": ['"Test"', '" no Test"'], + "alternatives": ['"Test"', '"no Test"'], }, "children": [ { diff --git a/api/src/v0/services/structure_utils/__init__.py b/api/src/v0/services/analysis/__init__.py similarity index 100% rename from api/src/v0/services/structure_utils/__init__.py rename to api/src/v0/services/analysis/__init__.py diff --git a/api/src/v0/services/analysis/id_to_dt.py b/api/src/v0/services/analysis/id_to_dt.py new file mode 100644 index 0000000..3b76099 --- /dev/null +++ b/api/src/v0/services/analysis/id_to_dt.py @@ -0,0 +1,168 @@ +""" +Conversion of influence diagram to decision tree. +The decision tree format is used for display in the frontend. +""" + +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.decision_tree import DecisionTree +from src.v0.services.classes.influence_diagram import InfluenceDiagram +from src.v0.services.classes.node import NodeABC, UtilityNode +from src.v0.services.errors import PartialOrderOutputModeError + + +class InfluenceDiagramToDecisionTree: + def decision_elimination_order( + self, influence_diagram: InfluenceDiagram + ) -> list[NodeABC]: + """Decision Elimination Order algorithm + + Args: + influence_diagram (InfluenceDiagram): the influence diagram object + to convert + + Returns: + list[NodeABC] : the decision elimination order graph associated to + the influence diagram. Nodes in the list are copies of the + nodes of the influence diagram ones. + + TODO: add description of what is the algorithm about + """ + cid_copy = influence_diagram.copy() + decisions = [] + decisions_count = cid_copy.decision_count + while decisions_count > 0: + nodes = list(cid_copy.graph.nodes()) + for node in nodes: + if not cid_copy.has_children(node): + if node.is_decision_node: + decisions.append(node) + decisions_count -= 1 + cid_copy.graph.remove_node(node) + return decisions + + def calculate_partial_order( + self, influence_diagram: InfluenceDiagram, *, mode="view" + ) -> list[NodeABC]: + """Partial order algorithm + + + Args: + influence_diagram (InfluenceDiagram): the influence diagram object + to convert + mode (str): ["view"(default)|"copy"] + returns a view or a copy of the nodes + + Returns + List[NodeABC]: list of nodes (copies or vioews) sorted in decision order + + TODO: add description of what the algorithm is about + TODO: handle utility nodes + """ + if mode not in ["view", "copy"]: + raise PartialOrderOutputModeError(mode) + + # get all chance nodes + uncertainty_node = influence_diagram.get_uncertainty_nodes() + elimination_order = self.decision_elimination_order(influence_diagram) + # TODO: Add utility nodes + partial_order = [] + + while elimination_order: + decision = elimination_order.pop() + parent_decision_nodes = [] + for parent in influence_diagram.get_parents(decision): + if not parent.is_decision_node: + if parent in uncertainty_node: + parent_decision_nodes.append(parent) + uncertainty_node.remove(parent) + + if len(parent_decision_nodes) > 0: + partial_order += parent_decision_nodes + partial_order.append(decision) + + partial_order += uncertainty_node + + if mode == "copy": + partial_order = [node.copy() for node in partial_order] + + return partial_order + + def _output_branches_from_node( + self, node: NodeABC, node_in_partial_order: NodeABC, flip=True + ) -> list[tuple[Arc, NodeABC]]: + """Make a list of output branches from a node + + This method actually returns the states of the nodes. + + Args + node (NodeABC): node to find the output branch from + node_in_partial_order (NodeABC): associated node in the partial order - to + keep reference too + flip (bool): if True (default), flip the list of branches so a generated + decision tree is in the same order as the entered states. + If False, the tree will be flipped horizontally. + + Returns + List: the list of tuples (Arc, Node in partial order) + The edges have the input node as start endpoint and name given by the + state + """ + if node.is_utility_node: + tree_stack = [ + Arc(tail=node, head=None, label=utility) for utility in node.utility + ] + if node.is_decision_node: + tree_stack = [ + Arc(tail=node, head=None, label=alternative) + for alternative in node.alternatives + ] + if node.is_uncertainty_node: + # This needs to be re-written according to the way we deal with probabilities + tree_stack = [ + Arc(tail=node, head=None, label=outcome) for outcome in node.outcomes + ] + if flip: + tree_stack.reverse() + + return zip(tree_stack, [node_in_partial_order] * len(tree_stack), strict=False) + + def conversion(self, influence_diagram: InfluenceDiagram) -> DecisionTree: + """Convert the influence diagram into a DecisionTree object + + Returns: + DecisionTree: The symmetric decision tree equivalent to the influence diagram + + TODO: Update ID2DT according to way we deal with probabilities + """ + partial_order = self.calculate_partial_order(influence_diagram) + root_node = partial_order[0] + # decision_tree = DecisionTree.initialize_with_root(root_node) + decision_tree = DecisionTree(root=root_node) + # tree_stack contains views of the partial order nodes + # decision_tree contains copy of the nodes (as they appear several times) + tree_stack = [(root_node, root_node)] + + while tree_stack: + element = tree_stack.pop() + + if isinstance(element[0], NodeABC): + tree_stack += self._output_branches_from_node(*element) + + else: # element is a branch + tail_index = partial_order.index(element[1]) + + if tail_index < len(partial_order) - 1: + head = partial_order[tail_index + 1].copy() + tree_stack.append((head, partial_order[tail_index + 1])) + else: + # head = UtilityNode( + # name=element[0].name, tag=element[0].name.lower() + # ) + head = UtilityNode(shortname="ut", description="Utility") + + element[0].head = head + decision_tree.add_arc( + element[0] + ) # node is added when the branch is added + + return decision_tree diff --git a/api/src/v0/services/analysis/id_to_pyagrum.py b/api/src/v0/services/analysis/id_to_pyagrum.py new file mode 100644 index 0000000..7344971 --- /dev/null +++ b/api/src/v0/services/analysis/id_to_pyagrum.py @@ -0,0 +1,137 @@ +""" +Conversion of influence diagram to pyAgrum format. + +.. seealso: + pyAgrum documentation https://pyagrum.readthedocs.io/ +""" + +from itertools import product + +import pyAgrum as gum + +from src.v0.services.classes.discrete_conditional_probability import ( + DiscreteConditionalProbability, +) +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) +from src.v0.services.classes.influence_diagram import InfluenceDiagram +from src.v0.services.classes.node import ( + DecisionNode, + UncertaintyNode, + UtilityNode, +) +from src.v0.services.errors import ( + ArcPyAgrumFormatError, + InfluenceDiagramNotAcyclicError, + ProbabilityPyAgrumFormatError, +) + + +class InfluenceDiagramToPyAgrum: + def probabilities_conversion(self, probability): + if isinstance(probability, DiscreteConditionalProbability): + return self.conditional_probabilities_conversion(probability) + if isinstance(probability, DiscreteUnconditionalProbability): + return self.unconditional_probabilities_conversion(probability) + + def unconditional_probabilities_conversion(self, probability): + variables = probability.variables + if len(variables) != 1: + raise ProbabilityPyAgrumFormatError(variables) + return [ + ( + {}, + [ + probability._cpt.sel(**{variables[0]: state}) + for state in probability.outcomes + ], + ) + ] + + def conditional_probabilities_conversion(self, probability): + # agrum = list() + coords = probability._cpt.coords + variables = { + key: coords[key].data.tolist() for key in coords if key is not coords.dims[0] + } + agrum_dict = {k: list(range(len(v))) for k, v in variables.items()} + agrum_dict = [ + dict(zip(agrum_dict.keys(), values, strict=False)) + for values in product(*agrum_dict.values()) + ] + agrum_prob = [ + probability.get_distribution( + **{key: coords[key][val] for key, val in item.items()} + ).data.tolist() + for item in agrum_dict + ] + agrum = list(zip(agrum_dict, agrum_prob, strict=False)) + return agrum + + def nodes_conversion(self, nodes, gum_id): + # create an uuid for gum as 8 bytes integer and keep relation to uuid + uuid_dot_to_gum = {} + uuid_gum_to_dot = {} + node_uuid = {} + + for node in nodes: + labelized_variables = [node.shortname, node.description] + if isinstance(node, UncertaintyNode): + try: + labelized_variables.append(node.outcomes) + variable_id = gum_id.addChanceNode( + gum.LabelizedVariable(*labelized_variables) + ) + except Exception as e: + raise ProbabilityPyAgrumFormatError(e) + elif isinstance(node, DecisionNode): + # This works even when alternatives are [""] or None + labelized_variables.append(node.alternatives) + variable_id = gum_id.addDecisionNode( + gum.LabelizedVariable(*labelized_variables) + ) + elif isinstance(node, UtilityNode): + # Utility not yet implemented + labelized_variables.append(1) + variable_id = gum_id.addUtilityNode( + gum.LabelizedVariable(*labelized_variables) + ) + + uuid_dot_to_gum[node.uuid] = variable_id + uuid_gum_to_dot[variable_id] = node.uuid + node_uuid[variable_id] = node + + return uuid_dot_to_gum, uuid_gum_to_dot, node_uuid + + def arcs_conversion(self, arcs, gum_id, uuid_dot_to_gum): + for arc in arcs: + tail = uuid_dot_to_gum[arc.tail.uuid] + head = uuid_dot_to_gum[arc.head.uuid] + try: + gum_id.addArc(tail, head) + except Exception as e: + raise ArcPyAgrumFormatError(e) + return None + + def conversion(self, influence_diagram: InfluenceDiagram): + if not influence_diagram.is_acyclic: + raise InfluenceDiagramNotAcyclicError(False) + + gum_id = gum.InfluenceDiagram() + variable_id = [] + + uuid_dot_to_gum, uuid_gum_to_dot, node_uuid = self.nodes_conversion( + influence_diagram.nodes, gum_id + ) + # Add head and tail in gum_id + self.arcs_conversion(influence_diagram.arcs, gum_id, uuid_dot_to_gum) + + for variable_id in uuid_gum_to_dot: + if isinstance(node_uuid[variable_id], UncertaintyNode): + for agrum_prob in self.probabilities_conversion( + node_uuid[variable_id].probability + ): + gum_id.cpt(variable_id)[agrum_prob[0]] = agrum_prob[1] + + return gum_id diff --git a/api/src/v0/services/structure_utils/decision_diagrams/__init__.py b/api/src/v0/services/class_validations/__init__.py similarity index 100% rename from api/src/v0/services/structure_utils/decision_diagrams/__init__.py rename to api/src/v0/services/class_validations/__init__.py diff --git a/api/src/v0/services/class_validations/validate_and_set_arc.py b/api/src/v0/services/class_validations/validate_and_set_arc.py new file mode 100644 index 0000000..0859541 --- /dev/null +++ b/api/src/v0/services/class_validations/validate_and_set_arc.py @@ -0,0 +1,37 @@ +from typing import Any +from uuid import UUID, uuid4 + +from src.v0.services.classes.node import NodeABC +from src.v0.services.errors import ( + ArcLabelValidationError, + EndPointValidationError, + UUIDValidationError, +) + + +def label(arg: Any) -> str: + if not (isinstance(arg, str) or arg is None): + raise ArcLabelValidationError(arg) + return arg + + +def edge(arg: Any) -> NodeABC: + if not (isinstance(arg, NodeABC) or arg is None): + raise EndPointValidationError(arg) + return arg + + +def uuid(arg: Any) -> str: + if not (isinstance(arg, str | UUID) or arg is None): + raise UUIDValidationError(arg) + if arg is None: + return str(uuid4()) + if isinstance(arg, UUID) and arg.version == 4: + return str(arg) + try: # if arg is str + uuid_obj = UUID(arg) + if uuid_obj.version == 4: + return arg + except Exception as e: + raise UUIDValidationError(e) + raise UUIDValidationError(f"version {uuid_obj.version}") diff --git a/api/src/v0/services/class_validations/validate_and_set_graph_model.py b/api/src/v0/services/class_validations/validate_and_set_graph_model.py new file mode 100644 index 0000000..18bc707 --- /dev/null +++ b/api/src/v0/services/class_validations/validate_and_set_graph_model.py @@ -0,0 +1,36 @@ +from typing import Any + +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.node import ( + DecisionNode, + NodeABC, + UncertaintyNode, + UtilityNode, +) +from src.v0.services.errors import ( + ArcTypeValidationError, + DTNodeTypeValidationError, + IDNodeTypeValidationError, +) + + +def id_node(arg: Any) -> DecisionNode | UncertaintyNode | UtilityNode: + if not isinstance(arg, DecisionNode | UncertaintyNode | UtilityNode): + raise IDNodeTypeValidationError(arg) + return arg + + +def dt_node(arg: Any) -> DecisionNode | UncertaintyNode | UtilityNode: + if not isinstance(arg, DecisionNode | UncertaintyNode | UtilityNode): + raise DTNodeTypeValidationError(arg) + return arg + + +def arc_to_graph(arg: Any) -> tuple[tuple[NodeABC, NodeABC], dict]: + if not isinstance(arg, Arc): + raise ArcTypeValidationError(arg) + return (arg.tail, arg.head), { + "dtype": arg.dtype, + "label": arg.label, + "uuid": arg.uuid, + } diff --git a/api/src/v0/services/class_validations/validate_and_set_node.py b/api/src/v0/services/class_validations/validate_and_set_node.py new file mode 100644 index 0000000..29706bf --- /dev/null +++ b/api/src/v0/services/class_validations/validate_and_set_node.py @@ -0,0 +1,77 @@ +from collections.abc import Sequence +from typing import Any +from uuid import UUID, uuid4 + +from src.v0.services.classes.abstract_probability import ProbabilityABC +from src.v0.services.classes.discrete_conditional_probability import ( + DiscreteConditionalProbability, +) +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) +from src.v0.services.errors import ( + AlternativeValidationError, + DescriptionValidationError, + NameValidationError, + ProbabilityValidationError, + ShortnameValidationError, + UUIDValidationError, +) + + +def description(arg: Any) -> str: + if not isinstance(arg, str): + raise DescriptionValidationError(arg) + return arg + + +def name(arg: Any) -> str: + if not isinstance(arg, str): + raise NameValidationError(arg) + return arg + + +def shortname(arg: Any) -> str: + if not isinstance(arg, str): + raise ShortnameValidationError(arg) + return arg + + +def uuid(arg: Any) -> str: + if not (isinstance(arg, str | UUID) or arg is None): + raise UUIDValidationError(arg) + if arg is None: + return str(uuid4()) + if isinstance(arg, UUID) and arg.version == 4: + return str(arg) + try: # if arg is str + uuid_obj = UUID(arg) + if uuid_obj.version == 4: + return arg + except Exception as e: + raise UUIDValidationError(e) + raise UUIDValidationError(f"version {uuid_obj.version}") + + +def alternatives(arg: Any) -> Sequence | None: + if isinstance(arg, str): + raise AlternativeValidationError(arg) + if not (isinstance(arg, Sequence) or arg is None): + raise AlternativeValidationError(arg) + if arg is None: + return arg + if not all(isinstance(item, str) for item in arg): + raise AlternativeValidationError(arg) + if len(arg) != len(set(arg)): + raise AlternativeValidationError(arg) + return arg + + +def probability(arg: Any) -> ProbabilityABC: + if not ( + isinstance(arg, DiscreteConditionalProbability) + or isinstance(arg, DiscreteUnconditionalProbability) + or arg is None + ): + raise ProbabilityValidationError(arg) + return arg diff --git a/api/src/v0/services/class_validations/validate_and_set_probability.py b/api/src/v0/services/class_validations/validate_and_set_probability.py new file mode 100644 index 0000000..052ec6e --- /dev/null +++ b/api/src/v0/services/class_validations/validate_and_set_probability.py @@ -0,0 +1,86 @@ +import re +from typing import Any + +import numpy as np + +from src.v0.services.errors import ( + DiscreteConditionalProbabilityFunctionValidationError, + DiscreteProbabilityVariableValidationError, + DiscreteUnconditionalProbabilityFunctionValidationError, +) + + +def discrete_variables(arg: Any) -> dict: + if not isinstance(arg, dict): + raise DiscreteProbabilityVariableValidationError(arg) + if not all(isinstance(v, list | np.ndarray) for v in arg.values()): + raise DiscreteProbabilityVariableValidationError(arg) + try: + if any(max(np.asarray(v).shape) != np.asarray(v).size for v in arg.values()): + pass + except Exception as e: + raise DiscreteProbabilityVariableValidationError(e) + if any(max(np.asarray(v).shape) != np.asarray(v).size for v in arg.values()): + raise DiscreteProbabilityVariableValidationError(arg) + # remove white spaces as the string is used as variable name by xarray + return {re.sub(r"\s+", "_", k): v for k, v in arg.items()} + + +def discrete_conditional_probability_function( + arg: Any, conditioned_variables: dict, conditioning_variables: dict +) -> dict: + def isnormalized(arr, cond_variables, threshold=1e-6): + axis = tuple(a for a in range(len(cond_variables.keys()))) + return np.all(np.linalg.norm(arr.sum(axis=axis) - 1.0) < threshold) + + if not isinstance(arg, np.ndarray): + raise DiscreteConditionalProbabilityFunctionValidationError(arg) + + arg = arg.astype(float) # in case of None + + variables = {**conditioned_variables, **conditioning_variables} + has_consistent_shape = tuple([len(v) for v in variables.values()]) == arg.shape + is_all_nans = np.isnan(arg.astype(float)).all() + is_between_zero_and_one = np.all(np.logical_and(arg >= 0, arg <= 1)) + is_normalized = isnormalized(arg, conditioned_variables) + + if not has_consistent_shape: + raise DiscreteConditionalProbabilityFunctionValidationError(arg) + + if not (is_all_nans ^ is_between_zero_and_one): + raise DiscreteConditionalProbabilityFunctionValidationError(arg) + + if not (is_all_nans ^ is_normalized): + raise DiscreteConditionalProbabilityFunctionValidationError(arg) + + return arg + + +def discrete_unconditional_probability_function(arg: Any, variables: dict) -> dict: + def isnormalized(arr, threshold=1e-6): + return np.all(np.linalg.norm(arr.sum() - 1.0) < threshold) + + if not isinstance(arg, np.ndarray): + raise DiscreteUnconditionalProbabilityFunctionValidationError(arg) + + arg = arg.astype(float) # in case of None + + # if 1D case + # if np.prod(arg.shape) == np.prod(arg.flatten().shape): + if 1 in arg.shape: + arg = arg.flatten() + has_consistent_shape = tuple([len(v) for v in variables.values()]) == arg.shape + is_all_nans = np.isnan(arg).all() + is_between_zero_and_one = np.all(np.logical_and(arg >= 0, arg <= 1)) + is_normalized = isnormalized(arg) + + if not has_consistent_shape: + raise DiscreteUnconditionalProbabilityFunctionValidationError(arg) + + if not (is_all_nans ^ is_between_zero_and_one): + raise DiscreteUnconditionalProbabilityFunctionValidationError(arg) + + if not (is_all_nans ^ is_normalized): + raise DiscreteUnconditionalProbabilityFunctionValidationError(arg) + + return arg diff --git a/api/src/v0/services/structure_utils/probability/__init__.py b/api/src/v0/services/classes/__init__.py similarity index 100% rename from api/src/v0/services/structure_utils/probability/__init__.py rename to api/src/v0/services/classes/__init__.py diff --git a/api/src/v0/services/classes/abstract_directed_graph.py b/api/src/v0/services/classes/abstract_directed_graph.py new file mode 100644 index 0000000..55fe72f --- /dev/null +++ b/api/src/v0/services/classes/abstract_directed_graph.py @@ -0,0 +1,229 @@ +"""Module defining the ProbabilisticGraphModel Abstract class""" + +from __future__ import annotations + +from abc import ABC +from collections.abc import Sequence +from typing import TYPE_CHECKING + +import networkx as nx + +from src.v0.services.class_validations import validate_and_set_graph_model + +from ..errors import NodeInGraphError +from .arc import Arc + +if TYPE_CHECKING: # pragma: no cover + from .node import NodeABC + + +class DirectedGraphType: + @staticmethod + def get_validation_node_method(self): + if type(self).__qualname__ == "InfluenceDiagram": + method = "id_node" + elif type(self).__qualname__ == "DecisionTree": + method = "dt_node" + return getattr(validate_and_set_graph_model, method) + + +class DirectedGraphABC(ABC): + NODES_MODULE_PATH = "src.v0.services.classes.node" + + """Directed Graph + + While nodes are unique in the graph, it may exist several relationship + between them, expressed as the possibility to have several arcs between + two given nodes. + """ + + def __init__(self): + """ + Create an instance of a DirectedGraphABC (ABSTRACT class!!!) + It is a wrapper around networkx.Digraph + + Attributes: + graph: networkx object + """ + self.graph = nx.DiGraph() + + @property + def is_acyclic(self) -> bool: + """test if the graph is acyclic or not. + + An influence diagram or a decision tree are acyclic. However, during + the building process, the condition may not be true. + + Returns: + bool: True if the graph is acyclic, False otherwise. + """ + return nx.is_directed_acyclic_graph(self.graph) + + @property + def nodes(self) -> list[NodeABC]: + """List of the graph nodes + + Returns: + List[NodeABC]: the list of node instances + """ + return list(self.graph.nodes) + + @property + def arcs(self) -> list[Arc]: + """List of the graph arcs + + Returns: + List[Arc]: the list of arc instances + """ + g = nx.node_link_data(self.graph, source="tail", target="head", link="edges") + return [ + Arc( + tail=item["tail"], + head=item["head"], + label=item["label"], + unique_id=item["uuid"], + ) + for item in g["edges"] + ] + + @property + def node_uuids(self) -> list: + """List of uuid's of the graph nodes + + Returns: + List: the list of uuid's + """ + return [node.uuid for node in self.graph] + + def node_in(self, node: NodeABC) -> bool: + """Check if a node is in the graph. + + The test is done through the uuid of the nodes. + + Args: + node (NodeABC): the node to be tested + + Returns: + bool: True if the node is already within the graph, False otherwise. + """ + return node.uuid in self.node_uuids + + def add_nodes(self, nodes: Sequence[NodeABC]): + """Add a list/tuple of nodes to a graph + + Args: + nodes (Sequence[NodeABC]): list/tuple of the nodes to be added + """ + for node in nodes: + self.add_node(node) + + def add_arcs(self, arcs: Sequence[Arc]): + """Add a list/tuple of arcs to a graph. + Endpoints of the arcs which are not already in the graph are added. + + Args: + arcs (Sequence[Arcs]): list/tuple of the arcs to be added + """ + for arc in arcs: + self.add_arc(arc) + + def add_node(self, node: NodeABC): + """Add a node to the graph + + Args: + node (NodeABC): node to be added + """ + validation_method = DirectedGraphType.get_validation_node_method(self) + if not self.node_in(node): + self.graph.add_node(validation_method(node)) + + def add_arc(self, arc: Arc): + """Add an arc to the graph + + Args: + arc (Arc): arc to be added. If some of the end points do not exist, + they are added to the graph too. + """ + arc_info = validate_and_set_graph_model.arc_to_graph(arc) + tail = ( + arc_info[0][0] + if not self.node_in(arc_info[0][0]) + else self.get_node_from_uuid(arc_info[0][0].uuid) + ) + head = ( + arc_info[0][1] + if not self.node_in(arc_info[0][1]) + else self.get_node_from_uuid(arc_info[0][1].uuid) + ) + self.graph.add_edge(tail, head, **arc_info[1]) + + def copy(self): + """copy the probabilistic graph model + + Returns + ProbabilisticGraphModel: a copy of the graph. uuid's of + the nodes are copied too. + """ + new_graph = type(self)() # Need to instance from the concrete class + new_graph.graph = self.graph.copy() + return new_graph + + def get_parents(self, node: NodeABC) -> list[NodeABC]: + """get parents of a given node + + Args: + node (NodeABC): node to find the parents of + + Returns + List[NodeABC]: the list of the parents of the given node + """ + if not self.graph.has_node(node): + raise NodeInGraphError(node) + return list(self.graph.predecessors(node)) + + def get_children(self, node: NodeABC) -> list[NodeABC]: + """get children of a given node + + Args: + node (NodeABC): node to find the children of + + Returns + List[NodeABC]: the list of the children of the given node + """ + if not self.graph.has_node(node): + raise NodeInGraphError(node) + return list(self.graph.successors(node)) + + def has_children(self, node: NodeABC) -> bool: + """Check for existence of children + + Args: + node (NodeABC): node to check for children + + Returns + bool: existence (True) or not (False) of children to the given node + """ + return len(self.get_children(node)) > 0 + + def get_node_from_uuid(self, uuid: str) -> NodeABC: + """get a node given its uuid + + Args: + uuid (str): uuid of node to look for + + Returns: + NodeABC: node object having the given uuid + """ + return [node for node in self.graph if node.uuid == uuid][0] + + # def get_node_type(self, node: NodeABC) -> str: + # """Return the type of a given node + + # Args: + # node (NodeABC): node to examine + + # Returns + # str: a string describing the type of the node (class name in + # lower font and without the "Node" suffix) + # """ + # return type(node).__name__.replace("Node", "").lower() diff --git a/api/src/v0/services/classes/abstract_probability.py b/api/src/v0/services/classes/abstract_probability.py new file mode 100644 index 0000000..ead1351 --- /dev/null +++ b/api/src/v0/services/classes/abstract_probability.py @@ -0,0 +1,27 @@ +import logging +from abc import ABC, abstractmethod + +logger = logging.getLogger(__name__) + + +class ProbabilityABC(ABC): + """ProbabilityABC""" + + @classmethod + @abstractmethod + def initialize_nan(cls, *args, **kwargs): + raise NotImplementedError + + @classmethod + @abstractmethod + def initialize_uniform(cls, *args, **kwargs): + raise NotImplementedError + + @property + @abstractmethod + def variables(self): + raise NotImplementedError + + @abstractmethod + def get_distribution(self, **kwargs): + raise NotImplementedError diff --git a/api/src/v0/services/classes/arc.py b/api/src/v0/services/classes/arc.py new file mode 100644 index 0000000..899c413 --- /dev/null +++ b/api/src/v0/services/classes/arc.py @@ -0,0 +1,127 @@ +"""Arc classes + +Raises: + EndpointTypeError: When endpoint cannot be set to start or end + UtilityNodeSuccessorError: When a UtilityNode has a successor which + is not another UtilityNode +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.v0.services.classes.node import ( + DecisionNode, + UncertaintyNode, + UtilityNode, +) + +if TYPE_CHECKING: # pragma: no cover + from src.v0.services.classes.node import NodeABC + +from src.v0.services.class_validations import validate_and_set_arc + +from ..errors import UtilityNodeSuccessorError + + +class Arc: + """Class of arcs""" + + def __init__( + self, *, tail: NodeABC, head: NodeABC, label: str = None, unique_id=None + ): + """Instance of Arc + + Args: + tail (NodeABC): start node of the arc + head (NodeABC): end node of the arc + label (str, optional): label of the arc. Defaults to None. + unique_id (str, optional): uuid. Defaults to None. If None, + the uuid is attributed at instantiation. + + Private attributes: + _tail (NodeABC): start node of the arc + _head (NodeABC): end node of the arc + _label (str, optional): label of the arc. Defaults to None. + _uuid (str, optional): uuid. Defaults to None. If None, the uuid + is attributed at instantiation. + """ + self._tail = validate_and_set_arc.edge(tail) + self._head = validate_and_set_arc.edge(head) + self._label = validate_and_set_arc.label(label) + self._uuid = validate_and_set_arc.uuid(unique_id) + + @property + def tail(self) -> NodeABC: + """ + Returns: + NodeABC: the node at start of the arc (tail) + """ + return self._tail + + @property + def head(self) -> NodeABC: + """ + Returns: + NodeABC: the node at end of the arc (head) + """ + return self._head + + @property + def label(self) -> str: + """ + Returns: + str: the label of the arc + """ + return self._label + + @property + def uuid(self) -> str: + """Return the `uuid` attribute + + Returns: + str: uuid of the node + """ + return self._uuid + + @property + def dtype(self): + """type of the arc (informational/conditional)""" + if isinstance(self.head, DecisionNode): + return "informational" + elif isinstance(self.head, UncertaintyNode): + return "conditional" + elif isinstance(self.head, UtilityNode): + return "functional" + elif self.head is None: + return None + + @label.setter + def label(self, value): + self._label = validate_and_set_arc.label(value) + + @tail.setter + def tail(self, node): + if node.is_utility_node: + if not (self.head.is_utility_node or self.head is None): + raise UtilityNodeSuccessorError(f"{self.head.uuid}/{node.uuid}") + self._tail = validate_and_set_arc.edge(node) + + @head.setter + def head(self, node): + if self.tail.is_utility_node and not node.is_utility_node: + raise UtilityNodeSuccessorError(f"{self.tail.uuid}/{node.uuid}") + self._head = validate_and_set_arc.edge(node) + + def copy(self): + """copy an arc + + Returns: + Arc: copied arc, endpoints keeping their uuids + """ + # need to create the arc and then fill it so that + # the endpoints are the same including uuid + arc = Arc(tail=None, head=None, label=self.label) + arc.tail = self.tail + arc.head = self.head + return arc diff --git a/api/src/v0/services/classes/decision_tree.py b/api/src/v0/services/classes/decision_tree.py new file mode 100644 index 0000000..75c1079 --- /dev/null +++ b/api/src/v0/services/classes/decision_tree.py @@ -0,0 +1,59 @@ +"""Module defining the DecisionTree class + +A DecisionTree is a sub-class of DirectedGraphABC. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from src.v0.services.class_validations import validate_and_set_graph_model + +from .abstract_directed_graph import DirectedGraphABC + +if TYPE_CHECKING: # pragma: no cover + from src.v0.services.classes.node import NodeABC + + +class DecisionTree(DirectedGraphABC): + """Decision tree class""" + + def __init__(self, root: NodeABC = None): + """Create an instance of a DecisionTree + It is a wrapper around networkx.Digraph + + Attributes: + graph (networkx.DiGraph): networkx.DiGraph object containing the + decision tree (nodes and arcs) + """ + super().__init__() + if root is not None: + self.add_node(validate_and_set_graph_model.dt_node(root)) + + @property + def root(self): + """return the root node of the graph (no incoming arcs) + + Returns: + NodeABC | None : the root node in the graph or None if none (cyclic) or + more than 1 (graph under construction) + """ + root_node = [n for n, d in self.graph.in_degree() if d == 0] + if len(root_node) != 1: + return None + return root_node[0] + + def parent(self, node: NodeABC) -> NodeABC | None: + """return the parent node in the Decition Tree + + In a Decision Tree, a node has a unique parent + (The root doesn't have any) + + Args: + node (NodeABC): node to find the parent of + + Returns + NodeABC | None: the parent node or None is no parent node has been found + """ + parents = self.get_parents(node) + return parents[0] if len(parents) > 0 else None diff --git a/api/src/v0/services/classes/discrete_conditional_probability.py b/api/src/v0/services/classes/discrete_conditional_probability.py new file mode 100644 index 0000000..bdf6fb2 --- /dev/null +++ b/api/src/v0/services/classes/discrete_conditional_probability.py @@ -0,0 +1,163 @@ +import numpy as np +import xarray as xr +from numpy.typing import ArrayLike + +from src.v0.services.class_validations import validate_and_set_probability +from src.v0.services.classes.abstract_probability import ProbabilityABC + + +class DiscreteConditionalProbability(ProbabilityABC): + """DiscreteConditionalProbability""" + + def __init__(self, probability_function: ArrayLike, variables: dict) -> None: + """ + Parameters + ----------- + probability_function: ArrayLike + gives the probability that a variable is equal to some value + variables: dict + involves the variable name and its values; values refers to the + set of possible outcomes + + .. note: + Future: + + conditioned_variables: dict + involves the variable name and its values; values refers to the + set of possible outcomes + conditioning_variables: dict + involves the variable name and its values; values refers to the + set of possible outcomes + + Warning + -------- + Spaces in variable names and values will be removed. + + Notes + ----- + + For the Conditional Probability Table, P(Test Result | Test, State) such as + + || 1. Test || yes yes | no no + || 2. State || Peach | Lemon | Peach | Lemon + ----------------------------------------------------------------- + 0. Test Result || || | | | + ----------------------------------------------------------------- + no Test || || 0.1 | 0.05 | 0.85 | 0.46 + Peach || || 0.7 | 0.35 | 0.12 | 0.26 + Lemon || || 0.2 | 0.60 | 0.03 | 0.28 + + the probability_function will be + [ + [0.1, 0.05, 0.85, 0.46], + [0.7, 0.35, 0.12, 0.26], + [0.2, 0.60, 0.03, 0.28], + ] + + and the variables + { + "Test Result": ["no Test", "Peach", "Lemon"], + "Test": ["yes", "no"], + "State": ["Peach", "Lemon"], + } + """ + super().__init__() + variable_list = list(variables.keys()) + variable_values = list(variables.values()) + conditioned_variables = {variable_list[0]: variable_values[0]} + conditioning_variables = { + variable_list[k]: variable_values[k] for k in range(1, len(variable_list)) + } + conditioned_variables = validate_and_set_probability.discrete_variables( + conditioned_variables + ) + conditioning_variables = validate_and_set_probability.discrete_variables( + conditioning_variables + ) + probability_function = ( + validate_and_set_probability.discrete_conditional_probability_function( + probability_function, conditioned_variables, conditioning_variables + ) + ) + self._cpt = xr.DataArray( + probability_function, + coords={**conditioned_variables, **conditioning_variables}, + attrs={ + "conditioned_variables": list(conditioned_variables.keys()), + "conditioning_variables": list(conditioning_variables.keys()), + }, + ) + + @property + def outcomes(self): + variable_names = self.conditioned_variables + if len(variable_names) == 1: + return tuple(self._cpt.coords[variable_names[0]].data.tolist()) + # FUTURE IMPLEMENTATION + # else: + # return tuple( + # product( + # *tuple(tuple(self._cpt.coords[vn].data.tolist()) \ + # for vn in variable_names) + # ) + # ) + + @property + def variables(self): + return self._cpt.dims + + @property + def conditioned_variables(self): + return tuple( + item + for item in self._cpt.dims + if item in self._cpt.attrs["conditioned_variables"] + ) + + @property + def conditioning_variables(self): + return tuple( + item + for item in self._cpt.dims + if item in self._cpt.attrs["conditioning_variables"] + ) + + @classmethod + def initialize_nan(cls, conditioned_variables: dict, conditioning_variables: dict): + data_shape = tuple( + np.asarray(v).shape[0] + for v in {**conditioned_variables, **conditioning_variables}.values() + ) + data = np.full(data_shape, np.nan) + return cls( + probability_function=data, + variables=conditioned_variables | conditioning_variables, + ) + + @classmethod + def initialize_uniform( + cls, conditioned_variables: dict, conditioning_variables: dict + ): + data_shape = tuple( + np.asarray(v).shape[0] + for v in {**conditioned_variables, **conditioning_variables}.values() + ) + data = np.ones(data_shape) / data_shape[0] + return cls( + probability_function=data, + variables=conditioned_variables | conditioning_variables, + ) + + def get_distribution(self, **variables): + """Return the probability distribution + + Parameters + ---------- + **variables: + variables (name=value) for which the distribution is desired + + Return + ------ + xr.DataArray + """ + return self._cpt.sel(**variables) diff --git a/api/src/v0/services/classes/discrete_unconditional_probability.py b/api/src/v0/services/classes/discrete_unconditional_probability.py new file mode 100644 index 0000000..c56a3e7 --- /dev/null +++ b/api/src/v0/services/classes/discrete_unconditional_probability.py @@ -0,0 +1,109 @@ +from itertools import product + +import numpy as np +import xarray as xr +from numpy.typing import ArrayLike + +from src.v0.services.class_validations import validate_and_set_probability +from src.v0.services.classes.abstract_probability import ProbabilityABC + + +class DiscreteUnconditionalProbability(ProbabilityABC): + """DiscreteUnconditionalProbability""" + + def __init__(self, probability_function: ArrayLike, variables: dict) -> None: + """ + Parameters + ----------- + probability_function: ArrayLike + gives the probability that a variable is equal to some value + variables: dict + involves the variable name and its values; values refers to the set + of possible outcomes + + Warning + -------- + Spaces in variable names and values will be removed. + + Notes + ----- + + For the Probability P(A, B , C) such as + + || 1. B || b1 b1 | b2 b2 + || 2. C || c1 | c2 | c1 | c1 + ----------------------------------------------------------------- + 0. A || || | | | + ----------------------------------------------------------------- + a1 || || 0.00 | 0.13 | 0.20 | 0.06 + a2 || || 0.03 | 0.28 | 0.11 | 0.02 + a3 || || 0.12 | 0.04 | 0.10 | 0.01 + + the probability_function will be + [ + [0.00, 0.13, 0.20, 0.06], + [0.03, 0.18, 0.11, 0.02], + [0.12, 0.04, 0.10, 0.01], + ] + + and the variables + { + "A": ["a1", "a2", "a3"], + "B": ["b1", "b2"], + "C": ["c1", "c2"], + } + """ + super().__init__() + variables = validate_and_set_probability.discrete_variables(variables) + probability_function = ( + validate_and_set_probability.discrete_unconditional_probability_function( + probability_function, variables + ) + ) + self._cpt = xr.DataArray(probability_function, coords=variables) + + @property + def outcomes(self): + variable_names = self._cpt.dims + if len(variable_names) == 1: + return tuple(self._cpt.coords[variable_names[0]].data.tolist()) + else: + outcomes_as_tuples = tuple( + product( + *tuple( + tuple(self._cpt.coords[vn].data.tolist()) + for vn in variable_names + ) + ) + ) + return tuple(" - ".join(t) for t in outcomes_as_tuples) + + @property + def variables(self): + return self._cpt.dims + + @classmethod + def initialize_nan(cls, variables: dict): + data_shape = tuple(np.asarray(v).shape[0] for v in variables.values()) + data = np.full(data_shape, np.nan) + return cls(data, variables=variables) + + @classmethod + def initialize_uniform(cls, variables: dict): + data_shape = tuple(np.asarray(v).shape[0] for v in variables.values()) + data = np.ones(data_shape) / np.prod(data_shape) + return cls(data, variables=variables) + + def get_distribution(self, **variables): + """Return the probability distribution + + Parameters + ---------- + **variables: + variables (name=value) for which the distribution is desired + + Return + ------ + xr.DataArray + """ + return self._cpt.sel(**variables) diff --git a/api/src/v0/services/classes/influence_diagram.py b/api/src/v0/services/classes/influence_diagram.py new file mode 100644 index 0000000..80c8999 --- /dev/null +++ b/api/src/v0/services/classes/influence_diagram.py @@ -0,0 +1,90 @@ +"""Module defining the InfluenceDiagram class + +An InfluenceDiagram is a sub-class of DirectedGraphABC. +""" + +from src.v0.services.classes.node import ( + DecisionNode, + UncertaintyNode, + UtilityNode, +) + +from .abstract_directed_graph import DirectedGraphABC + + +class InfluenceDiagram(DirectedGraphABC): + """Influence Diagram""" + + def __init__(self): + """ + Create an instance of an InfluenceDiagram + It is a wrapper around networkx.Digraph + + Attributes: + graph (networkx.DiGraph): networkx.DiGraph object + containing the influence diagram (nodes and arcs) + """ + super().__init__() + + def get_decision_nodes(self) -> list[DecisionNode]: + """Return a list of decision nodes + + Returns: + list[DecisionNode] + """ + return [ + node[0] + for node in list(self.graph.nodes(data=True)) + if isinstance(node[0], DecisionNode) + ] + + def get_uncertainty_nodes(self) -> list[UncertaintyNode]: + """Return a list of uncertainty nodes + + Returns: + list[UncertaintyNode] + """ + return [ + node[0] + for node in list(self.graph.nodes(data=True)) + if isinstance(node[0], UncertaintyNode) + ] + + def get_utility_nodes(self) -> list[UtilityNode]: + """Return a list of utility nodes + + Returns: + list[UtilityNode] + """ + return [ + node[0] + for node in list(self.graph.nodes(data=True)) + if isinstance(node[0], UtilityNode) + ] + + @property + def decision_count(self) -> int: + """Return the number of decision nodes + + Returns: + int: the number of DecisionNode objects in the influence diagram + """ + return len(self.get_decision_nodes()) + + @property + def uncertainty_count(self) -> int: + """Return the number of uncertainty nodes + + Returns: + int: the number of UncertaintyNode objects in the influence diagram + """ + return len(self.get_uncertainty_nodes()) + + @property + def utility_count(self) -> int: + """Return the number of utility nodes + + Returns: + int: the number of UtilityNode objects in the influence diagram + """ + return len(self.get_utility_nodes()) diff --git a/api/src/v0/services/classes/node.py b/api/src/v0/services/classes/node.py new file mode 100644 index 0000000..cac5f5a --- /dev/null +++ b/api/src/v0/services/classes/node.py @@ -0,0 +1,287 @@ +"""Nodes classes + +NodeABC is an abstract class with 3 concretizations: +- DecisionNode +- UncertaintyNode +- UtilityNode +""" + +from abc import ABC, abstractmethod +from collections.abc import Sequence + +from src.v0.services.class_validations import validate_and_set_node + +from .abstract_probability import ProbabilityABC + + +class NodeABC(ABC): + """Abstract class of nodes""" + + def __init__(self, *, description: str, shortname: str, uuid=None): + """instantiation of an (abstract) node + + Args: + description (str): description of the node + shortname (str): shortname of the node + uuid (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + + Private attributes: + _description (str): description of the node + _shortname (str): shortname of the node + _uuid (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + """ + self._description = validate_and_set_node.description(description) + self._shortname = validate_and_set_node.shortname(shortname) + self._uuid = validate_and_set_node.uuid(uuid) + + @property + def description(self) -> str: + """Return the `description` attribute + + Returns: + str: description of the node + """ + return self._description + + @property + def shortname(self) -> str: + """Return the `shortname` attribute + + Returns: + str: shortname of the node + """ + return self._shortname + + @property + def uuid(self) -> str: + """Return the `uuid` attribute + + Returns: + str: uuid of the node + """ + return self._uuid + + @description.setter + def description(self, value): + self._description = validate_and_set_node.description(value) + + @shortname.setter + def shortname(self, value): + self._shortname = validate_and_set_node.shortname(value) + + @uuid.setter + def uuid(self, value): + self._uuid = validate_and_set_node.uuid(value) + + @property + def is_decision_node(self) -> bool: + """ + Returns: + bool: True of the node is a DecisionNode, false otherwise + """ + return isinstance(self, DecisionNode) + + @property + def is_uncertainty_node(self) -> bool: + """ + Returns: + bool: True of the node is a UncertaintyNode, false otherwise + """ + return isinstance(self, UncertaintyNode) + + @property + def is_utility_node(self) -> bool: + """ + Returns: + bool: True of the node is a UtilityNode, false otherwise + """ + return isinstance(self, UtilityNode) + + @property + @abstractmethod + def states(self): # pragma: no cover + raise NotImplementedError + + def copy(self): + """Copy of the node, associating a new uuid + + Returns: + NodeABC: a copy of the node, with a new uuid + """ + copied_node = type(self)(description=self.description, shortname=self.shortname) + for attribute, value in self.__dict__.items(): + if attribute not in ["_description", "_shortname", "_uuid"]: + setattr(copied_node, attribute, value) + return copied_node + + +class DecisionNode(NodeABC): + """Decision node class""" + + def __init__( + self, + *, + description: str, + shortname: str, + uuid=None, + alternatives: Sequence[str] = None, + ): + """Create an instance of a DecisionNode + + Args: + description (str): description of the node + shortname (str): shortname of the node + unique_id (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + alternatives (Sequence[str], optional): alternatives (states) + of the decision. Defaults to None. + + Private attributes: + _description (str): description of the node + _shortname (str): shortname of the node + _unique_id (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + _alternatives (Sequence[str], optional): alternatives (states) of + the decision. Defaults to None. + """ + super().__init__(description=description, shortname=shortname, uuid=uuid) + self._alternatives = validate_and_set_node.alternatives(alternatives) + + @property + def alternatives(self) -> list[str]: + """ + Returns: + list[str]: the alternatives (states) of the decision + """ + if isinstance(self._alternatives, list): + return self._alternatives + if isinstance(self._alternatives, Sequence): + return list(self._alternatives) + return [] + + @alternatives.setter + def alternatives(self, value): + self._alternatives = validate_and_set_node.alternatives(value) + + @property + def states(self) -> list[str]: + """ + Returns: + list[str]: the alternatives (states) of the decision + """ + return self.alternatives + + +class UncertaintyNode(NodeABC): + """Uncertainty (or chance) node class""" + + def __init__( + self, + *, + description: str, + shortname: str, + uuid=None, + probability: ProbabilityABC = None, + ): + """Create an instance of a UncertaintyNode + + Args: + description (str): description of the node + shortname (str): shortname of the node + unique_id (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + probability (ProbabilityABC, optional): probability associated to the node. + Defaults to None. + + Private attributes: + _description (str): description of the node + _shortname (str): shortname of the node + _unique_id (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + _probability (ProbabilityABC, optional): probability associated to the node. + Defaults to None. + """ + super().__init__(description=description, shortname=shortname, uuid=uuid) + self._probability = validate_and_set_node.probability(probability) + + @property + def probability(self) -> ProbabilityABC: + """ + Returns: + ProbabilityABC: the probabibility associated to the node + """ + return self._probability + + @probability.setter + def probability(self, value): + self._probability = validate_and_set_node.probability(value) + + @property + def states(self) -> list[str]: + """ + Returns: + list[str]: the outcomes (states) of the uncertainty + """ + return self.outcomes + + @property + def outcomes(self) -> list[str]: + """ + Returns: + list[str]: the outcomes (states) of the uncertainty + """ + if self.probability is None: + return () + return self._probability.outcomes + + +class UtilityNode(NodeABC): + """Utility (or value) node class""" + + def __init__(self, *, description: str, shortname: str, uuid=None): + """Create an instance of a UtilityNode + + Args: + description (str): description of the node + shortname (str): shortname of the node + unique_id (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + + Private attributes: + _description (str): description of the node + _shortname (str): shortname of the node + _unique_id (str, optional): uuid. Defaults to None. + If None, the uuid is attributed at instantiation. + + TODO: implement utility matrix attribute through a Utility class. + So far it is rather a place holder. + TODO: implement the value metric in addition to the consequence + TODO: the total objective is a combination of gains and costs of each + decision and how we combine them into a desired objective + """ + super().__init__(description=description, shortname=shortname, uuid=uuid) + self._utility = None # TODO: implement it! + + @property + def utility(self) -> list[str]: + """ + Returns: + list[str]: the utility (states) of the utility + """ + if isinstance(self._utility, Sequence): + return list(self._utility) + return [] + + @utility.setter + def utility(self, value): + self._utility = value + + @property + def states(self) -> list[str]: + """ + Returns: + list[str]: the consequence entries (states) of the utility + """ + return self.utility diff --git a/api/src/v0/services/errors.py b/api/src/v0/services/errors.py new file mode 100644 index 0000000..d70259d --- /dev/null +++ b/api/src/v0/services/errors.py @@ -0,0 +1,252 @@ +""" +Error for services.classes errors +""" + +import logging + +logger = logging.getLogger(__name__) + + +class ProbabilityTypeError(Exception): + def __init__(self, arg): + error_message = f"Unreckonized probability type: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class DiscreteConditionalProbabilityTypeError(Exception): + def __init__(self, arg): + error_message = ( + f"Data cannot be used to create a DiscreteConditionalProbability: {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class DiscreteUnconditionalProbabilityTypeError(Exception): + def __init__(self, arg): + error_message = ( + f"Data cannot be used to create a DiscreteUnconditionalProbability: {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class DecisionNodeTypeError(Exception): + def __init__(self, arg): + error_message = f"Data cannot be used to create a DecisionNode: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class UncertaintyNodeTypeError(Exception): + def __init__(self, arg): + error_message = f"Data cannot be used to create a UncertaintyNode: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class UtilityNodeTypeError(Exception): + def __init__(self, arg): + error_message = f"Data cannot be used to create a UtilityNode: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class UtilityNodeSuccessorError(Exception): + def __init__(self, arg): + error_message = ( + f"Utility node can only have other utility nodes as successor: {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class NodeInGraphError(Exception): + def __init__(self, arg): + error_message = f"The node is not in the graph: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class InfluenceDiagramNodeTypeError(Exception): + def __init__(self, arg): + error_message = f"Data cannot be used to create an influence diagram Node: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class NodeTypeError(Exception): + def __init__(self, arg): + error_message = f"Data is not an InfluenceDiagram Node: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class DescriptionValidationError(Exception): + def __init__(self, arg): + error_message = f"Input description is not a string: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class NameValidationError(Exception): + def __init__(self, arg): + error_message = f"Input name is not a string: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class ArcLabelValidationError(Exception): + def __init__(self, arg): + error_message = f"Input label is neither a string nor None: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class EndPointValidationError(Exception): + def __init__(self, arg): + error_message = f"Endpoint of arcs should be Node or None: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class ArcTypeError(Exception): + def __init__(self, arg): + error_message = f"Data cannot be used to create an Arc: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class InfluenceDiagramTypeError(Exception): + def __init__(self, arg): + error_message = f"Data cannot be used to create an InfluenceDiagram: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class ArcTypeValidationError(Exception): + def __init__(self, arg): + error_message = f"Added arc is not of instance Arc: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class IDNodeTypeValidationError(Exception): + def __init__(self, arg): + error_message = ( + f"Added node is not of instance " + f"(DecisionNode, UncertaintyNode, UtilityNode): {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class DTNodeTypeValidationError(Exception): + def __init__(self, arg): + error_message = ( + f"Added node is not of instance " + f"(DecisionNode, UncertaintyNode, UtilityNode): {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class ShortnameValidationError(Exception): + def __init__(self, arg): + error_message = f"Input shortname is not a string: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class UUIDValidationError(Exception): + def __init__(self, arg): + error_message = f"Input uuid is neither a valid uuid (version 4) nor None: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class AlternativeValidationError(Exception): + def __init__(self, arg): + error_message = ( + f"Input alternatives is neither a list or " + f"tuple of unique strings nor None: {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class DiscreteProbabilityVariableValidationError(Exception): + def __init__(self, arg): + error_message = ( + f"One of the variables is not a dictionary with " + f"element being able to be interpreted as 1D: {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class DiscreteConditionalProbabilityFunctionValidationError(Exception): + def __init__(self, arg): + error_message = ( + f"The conditional probability function is not well formed size " + f"(not compatible with variables or content is not normalized): {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class DiscreteUnconditionalProbabilityFunctionValidationError(Exception): + def __init__(self, arg): + error_message = ( + f"The unconditional probability function is not well formed size " + f"(not compatible with variables or content is not normalized): {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class ProbabilityValidationError(Exception): + def __init__(self, arg): + error_message = ( + f"Input probability is neither a well formed probability nor None: {arg}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class InfluenceDiagramNotAcyclicError(Exception): + def __init__(self, arg): + error_message = f"the influence diagram is not acyclic: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class RootNodeNotFound(Exception): + def __init__(self, arg): + error_message = f"Decision tree has no defined root node: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class PartialOrderOutputModeError(Exception): + def __init__(self, mode): + error_message = ( + f"output mode should be [view|copy] and have been entered as {mode}" + ) + super().__init__(error_message) + logger.critical(error_message) + + +class ArcPyAgrumFormatError(Exception): + def __init__(self, arg): + error_message = f"Input arc cannot be used in pyagrum with error: {arg}" + super().__init__(error_message) + logger.critical(error_message) + + +class ProbabilityPyAgrumFormatError(Exception): + def __init__(self, arg): + error_message = f"Input probability cannot be used in pyagrum with error: {arg}" + super().__init__(error_message) + logger.critical(error_message) diff --git a/api/tests/v0/services/structure_utils/__init__.py b/api/src/v0/services/format_conversions/__init__.py similarity index 100% rename from api/tests/v0/services/structure_utils/__init__.py rename to api/src/v0/services/format_conversions/__init__.py diff --git a/api/src/v0/services/format_conversions/arc.py b/api/src/v0/services/format_conversions/arc.py new file mode 100644 index 0000000..3a451c9 --- /dev/null +++ b/api/src/v0/services/format_conversions/arc.py @@ -0,0 +1,33 @@ +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.node import NodeABC +from src.v0.services.errors import ArcTypeError + +from .node import InfluenceDiagramNodeConversion + + +class ArcConversion: + def from_json(self, edge: dict, issues: list[dict]) -> Arc: + if not ("outV" in edge.keys() and "inV" in edge.keys()): + raise ArcTypeError(list(edge.keys())) + + tails = [issue for issue in issues if issue["uuid"] == edge["outV"]] + heads = [issue for issue in issues if issue["uuid"] == edge["inV"]] + + if not (len(tails) == 1 and len(heads) == 1): + raise ArcTypeError((tails, heads)) + + return Arc( + tail=InfluenceDiagramNodeConversion().from_json(tails[0]), + head=InfluenceDiagramNodeConversion().from_json(heads[0]), + ) + + def to_json(self, arc: Arc, nodes: list[NodeABC]) -> dict: + outV = [node.uuid for node in nodes if node == arc.tail][0] + inV = [node.uuid for node in nodes if node == arc.head][0] + return { + "outV": outV, + "inV": inV, + "uuid": arc.uuid, + "id": arc.uuid, + "label": "influences", + } diff --git a/api/src/v0/services/format_conversions/base.py b/api/src/v0/services/format_conversions/base.py new file mode 100644 index 0000000..1510077 --- /dev/null +++ b/api/src/v0/services/format_conversions/base.py @@ -0,0 +1,60 @@ +""" +General utilities for data format conversions between database and service layer- +""" + +import datetime +from abc import ABC, abstractmethod + +from src.v0.models.meta import EdgeMetaDataResponse, VertexMetaDataResponse + + +class ConversionABC(ABC): + """ + Abstract class for data format conversions between database and service layer. + """ + + @abstractmethod + def to_json(self, data): + raise NotImplementedError + + @abstractmethod + def from_json(self, data): + raise NotImplementedError + + +class MetadataCreate: + """ + Creation of metadata response + """ + + def vertex(uuid: str) -> VertexMetaDataResponse: + """Create metadata for vertex + + Args: + uuid (str): UUID of vertex as string + + Returns: + VertexMetaDataResponse: vertex metadata + """ + version = "v0" + ct = datetime.datetime.now() + date = str(ct) + timestamp = str(ct.timestamp()) + uuid = uuid + return VertexMetaDataResponse.model_validate( + {"version": version, "date": date, "timestamp": timestamp, "uuid": uuid} + ) + + def edge(uuid: str) -> EdgeMetaDataResponse: + """Create metadata for edge + + Args: + uuid (str): UUID of edge as string + + Returns: + EdgeMetaDataResponse: edge metadata + + """ + version = "v0" + uuid = uuid + return EdgeMetaDataResponse.model_validate({"version": version, "uuid": uuid}) diff --git a/api/src/v0/services/format_conversions/directed_graph.py b/api/src/v0/services/format_conversions/directed_graph.py new file mode 100644 index 0000000..c27ffd5 --- /dev/null +++ b/api/src/v0/services/format_conversions/directed_graph.py @@ -0,0 +1,79 @@ +import json + +import networkx as nx + +from src.v0.services.classes.decision_tree import DecisionTree +from src.v0.services.classes.influence_diagram import InfluenceDiagram +from src.v0.services.errors import InfluenceDiagramTypeError, RootNodeNotFound +from src.v0.services.format_conversions.node import DecisionTreeNodeConversion + +from .arc import ArcConversion +from .node import InfluenceDiagramNodeConversion + + +class InfluenceDiagramConversion: + def from_json(self, influence_diagram: dict) -> InfluenceDiagram: + if (nodes := influence_diagram.get("vertices", None)) is None: + raise InfluenceDiagramTypeError(None) + + diagram = InfluenceDiagram() + try: + for node_of_id in nodes: + diagram.add_node(InfluenceDiagramNodeConversion().from_json(node_of_id)) + diagram.add_arcs( + [ + ArcConversion().from_json(arc, nodes) + for arc in influence_diagram["edges"] + ] + ) + return diagram + except Exception: + raise InfluenceDiagramTypeError(None) + + def to_json(self, influence_diagram: InfluenceDiagram) -> dict: + return { + "vertices": [ + InfluenceDiagramNodeConversion().to_json(item) + for item in influence_diagram.nodes + ], + "edges": [ + ArcConversion().to_json(item, influence_diagram.nodes) + for item in influence_diagram.arcs + ], + } + + +class DecisionTreeConversion: + def from_json(self, decision_tree: dict) -> DecisionTree: + raise NotImplementedError + + def to_json(self, decision_tree: DecisionTree) -> dict: + def propagate_branch_name(decision_tree, node, names): + predecessor = decision_tree.parent(node) + if not predecessor: + return "" + n = names[(predecessor, node)] + return n if isinstance(n, str) else "-".join(n) + + if decision_tree.root is None: + raise RootNodeNotFound(None) + + edges_name = nx.get_edge_attributes(decision_tree.graph, "label") + tg = nx.readwrite.json_graph.tree_data(decision_tree.graph, decision_tree.root) + json_object = json.dumps( + tg, + default=lambda node: { + **DecisionTreeNodeConversion().to_json( + DecisionTreeNodeConversion().from_json( + InfluenceDiagramNodeConversion().to_json(node) + | { + "branch_name": propagate_branch_name( + decision_tree, node, edges_name + ) + } + ) + ) + }, + indent=2, + ) + return json.loads(json_object) diff --git a/api/src/v0/services/format_conversions/node.py b/api/src/v0/services/format_conversions/node.py new file mode 100644 index 0000000..b9aa308 --- /dev/null +++ b/api/src/v0/services/format_conversions/node.py @@ -0,0 +1,237 @@ +""" +This module converts issue related data between the database formats +and the service layer formats +""" + +from collections.abc import Sequence + +from src.v0.models.structure import DecisionTreeNodeData +from src.v0.services.classes.node import ( + DecisionNode, + NodeABC, + UncertaintyNode, + UtilityNode, +) +from src.v0.services.errors import ( + DecisionNodeTypeError, + InfluenceDiagramNodeTypeError, + NodeTypeError, + UncertaintyNodeTypeError, + UtilityNodeTypeError, +) + +from .base import ConversionABC, MetadataCreate +from .probability import ProbabilityConversion + + +def add_metadata(uuid: str) -> dict: + metadata = MetadataCreate.vertex(uuid) + return { + "uuid": metadata.uuid, + "timestamp": metadata.timestamp, + "date": metadata.date, + "id": metadata.uuid, + "label": "issue", + } + + +class DecisionJSONConversion: + """ + Conversion to json of fields relevant for decision + """ + + def states(self, node): + return ( + None + if (isinstance(node.alternatives, Sequence) and len(node.alternatives) == 0) + else node.alternatives + ) + + def decision_type(self, node): + return "Focus" + + +class UncertaintyJSONConversion: + """ + Conversion to json of fields relevant for uncertainty + """ + + def probability(self, node): + return ( + None + if (node.probability is None) + else ProbabilityConversion().to_json(node.probability) + ) + + def key_uncertainty(self, node): + return "true" + + def source(self, node): + return "" + + +class DecisionNodeConversion(ConversionABC): + """ + Concrete implementation of `from_json` and `to_json` for decisions. + """ + + def from_json(self, issue: dict) -> DecisionNode: + if issue.get("category") != "Decision": + raise DecisionNodeTypeError(issue.get("category")) + return DecisionNode( + description=issue.get("description"), + shortname=issue.get("shortname"), + uuid=issue.get("uuid"), + alternatives=issue.get("alternatives"), + ) + + def to_json(self, node: DecisionNode) -> dict: + data = { + "category": "Decision", + "shortname": node.shortname, + "description": node.description, + "alternatives": DecisionJSONConversion().states(node), + "decisionType": DecisionJSONConversion().decision_type(node), + "boundary": "in", + } + return data | add_metadata(node.uuid) + + +class UncertaintyNodeConversion(ConversionABC): + """ + Concrete implementation of `from_json` and `to_json` for uncertainties. + """ + + def from_json(self, issue: dict) -> UncertaintyNode: + if issue.get("category") != "Uncertainty": + raise UncertaintyNodeTypeError(issue.get("category")) + try: + probability = ProbabilityConversion().from_json(issue.get("probabilities")) + except Exception as e: + raise UncertaintyNodeTypeError(e) + return UncertaintyNode( + description=issue.get("description"), + shortname=issue.get("shortname"), + uuid=issue.get("uuid"), + probability=probability, + ) + + def to_json(self, node: UncertaintyNode) -> dict: + try: + probability = ProbabilityConversion().to_json(node.probability) + except Exception as e: + raise UncertaintyNodeTypeError(e) + data = { + "category": "Uncertainty", + "shortname": node.shortname, + "description": node.description, + "probabilities": probability, + "keyUncertainty": "true", + "boundary": "in", + } + return data | add_metadata(node.uuid) + + +class UtilityNodeConversion(ConversionABC): + """ + Concrete implementation of `from_json` and `to_json` for utilities. + + .. warning: + In this implementation we assume Utility is Value Metric which is not the + case and will be modified in future version + """ + + def from_json(self, issue: dict) -> UtilityNode: + if issue.get("category") != "Value Metric": + raise UtilityNodeTypeError(issue.get("category")) + return UtilityNode( + description=issue.get("description"), + shortname=issue.get("shortname"), + uuid=issue.get("uuid"), + ) + + def to_json(self, node: UtilityNode) -> dict: + data = { + "category": "Utility", + "shortname": node.shortname, + "description": node.description, + "boundary": "in", + } + return data | add_metadata(node.uuid) + + +class InfluenceDiagramNodeConversion(ConversionABC): + def from_json(self, issue: dict) -> NodeABC: + """Create a node from a json stream. + + Only key uncertainties, focus decisions and utilities are converted. + + Args: + issue (Dict): issue as a dictionary + + Raises: + InfluenceDiagramTypeError: _description_ + + Returns: + NodeABC: The converted node. + """ + if issue.get("category") not in ["Decision", "Uncertainty", "Value Metric"]: + raise InfluenceDiagramNodeTypeError(f'category: {issue.get("category")}') + if issue.get("boundary") not in ["in", "on"]: + raise InfluenceDiagramNodeTypeError(f'boundary: {issue.get("boundary")}') + if not issue.get("shortname"): + raise InfluenceDiagramNodeTypeError(f'shortname: {issue.get("shortname")}') + if issue["category"] == "Decision": + if issue.get("decisionType") != "Focus": + raise InfluenceDiagramNodeTypeError( + f'decisionType: {issue.get("decisionType")}' + ) + return DecisionNodeConversion().from_json(issue) + if issue["category"] == "Uncertainty": + if issue["keyUncertainty"] != "true": + raise InfluenceDiagramNodeTypeError( + f'keyUncertainty: {issue.get("keyUncertainty")}' + ) + return UncertaintyNodeConversion().from_json(issue) + if issue["category"] == "Value Metric": + return UtilityNodeConversion().from_json(issue) + + def to_json(self, node: NodeABC) -> dict: + if isinstance(node, DecisionNode): + return DecisionNodeConversion().to_json(node) + if isinstance(node, UncertaintyNode): + return UncertaintyNodeConversion().to_json(node) + if isinstance(node, UtilityNode): + return UtilityNodeConversion().to_json(node) + raise NodeTypeError(node) + + +class DecisionTreeNodeConversion(ConversionABC): + def from_json(self, node: dict) -> DecisionTreeNodeData: + return DecisionTreeNodeData.model_validate( + { + "node_type": node.get("category"), + "shortname": node.get("shortname"), + "description": node.get("description"), + "branch_name": node.get("branch_name"), + "alternatives": node.get("alternatives"), + "probabilities": node.get("probabilities"), + "utility": node.get("utility"), + "uuid": node.get("uuid"), + } + ) + + def to_json(self, node: DecisionTreeNodeData) -> dict: + data = { + "node_type": node.node_type, + "shortname": node.shortname, + "description": node.description, + "branch_name": node.branch_name, + "alternatives": node.alternatives, + "probabilities": node.probabilities.model_dump() + if node.probabilities + else None, + "utility": node.utility, + "uuid": node.uuid, + } + return data diff --git a/api/src/v0/services/format_conversions/probability.py b/api/src/v0/services/format_conversions/probability.py new file mode 100644 index 0000000..0a52111 --- /dev/null +++ b/api/src/v0/services/format_conversions/probability.py @@ -0,0 +1,94 @@ +""" +This module converts probability related data between the database formats +and the service layer formats +""" + +import numpy as np + +from src.v0.services.classes.abstract_probability import ProbabilityABC +from src.v0.services.classes.discrete_conditional_probability import ( + DiscreteConditionalProbability, +) +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) +from src.v0.services.errors import ( + DiscreteConditionalProbabilityTypeError, + DiscreteUnconditionalProbabilityTypeError, + ProbabilityTypeError, +) + +from .base import ConversionABC + + +class DiscreteConditionalProbabilityConversion(ConversionABC): + def from_json(self, probability: dict) -> DiscreteConditionalProbability: + if probability.get("dtype") != "DiscreteConditionalProbability": + raise DiscreteConditionalProbabilityTypeError(probability.get("dtype")) + array_size = tuple([len(v) for v in probability["variables"].values()]) + distribution = np.reshape( + np.asarray(probability["probability_function"]), array_size + ) + return DiscreteConditionalProbability(distribution, probability["variables"]) + + def to_json(self, probability: DiscreteConditionalProbability) -> dict: + array_size = ( + probability._cpt.data.shape[0], + np.astype(np.prod(probability._cpt.data.shape[1:]), int), + ) + distribution = np.reshape(probability._cpt.data, array_size).tolist() + + return { + "dtype": "DiscreteConditionalProbability", + "probability_function": distribution, + "variables": { + k: v.data.tolist() for k, v in probability._cpt.coords.items() + }, + } + + +class DiscreteUnconditionalProbabilityConversion(ConversionABC): + def from_json(self, probability: dict) -> DiscreteUnconditionalProbability: + if probability.get("dtype") != "DiscreteUnconditionalProbability": + raise DiscreteUnconditionalProbabilityTypeError(probability.get("dtype")) + array_size = tuple([len(v) for v in probability["variables"].values()]) + distribution = np.reshape( + np.asarray(probability["probability_function"]), array_size + ) + return DiscreteUnconditionalProbability(distribution, probability["variables"]) + + def to_json(self, probability: DiscreteUnconditionalProbability) -> dict: + array_size = ( + probability._cpt.data.shape[0], + np.astype(np.prod(probability._cpt.data.shape[1:]), int), + ) + distribution = np.reshape(probability._cpt.data, array_size).tolist() + return { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": distribution, + "variables": { + k: v.data.tolist() for k, v in probability._cpt.coords.items() + }, + } + + +class ProbabilityConversion(ConversionABC): + def from_json(self, probability: dict | None) -> ProbabilityABC: + if probability is None: + return None + if not isinstance(probability, dict): + raise ProbabilityTypeError(probability) + if probability.get("dtype") == "DiscreteUnconditionalProbability": + return DiscreteUnconditionalProbabilityConversion().from_json(probability) + if probability.get("dtype") == "DiscreteConditionalProbability": + return DiscreteConditionalProbabilityConversion().from_json(probability) + raise ProbabilityTypeError(probability) + + def to_json(self, probability: ProbabilityABC | None) -> dict: + if probability is None: + return None + if isinstance(probability, DiscreteUnconditionalProbability): + return DiscreteUnconditionalProbabilityConversion().to_json(probability) + if isinstance(probability, DiscreteConditionalProbability): + return DiscreteConditionalProbabilityConversion().to_json(probability) + raise ProbabilityTypeError(probability) diff --git a/api/src/v0/services/structure.py b/api/src/v0/services/structure.py index c1bc6fe..fa27ae8 100644 --- a/api/src/v0/services/structure.py +++ b/api/src/v0/services/structure.py @@ -1,7 +1,7 @@ -import json - -from src.v0.services.structure_utils.decision_diagrams.influence_diagram import ( - InfluenceDiagram, +from src.v0.services.analysis.id_to_dt import InfluenceDiagramToDecisionTree +from src.v0.services.format_conversions.directed_graph import ( + DecisionTreeConversion, + InfluenceDiagramConversion, ) from ..models.filter import Filter @@ -52,13 +52,13 @@ def read_influence_diagram(self, project_uuid: str) -> InfluenceDiagramResponse: issues_list = [ IssueResponse.model_validate(v.model_dump()) for v in vertices if v ] - edges = edge_repository.read_all_edges_from_sub_project( + arcs = edge_repository.read_all_edges_from_sub_project( project_uuid=project_uuid, edge_label="influences", vertex_uuid=[issue.uuid for issue in issues_list], ) - influence_diagram = InfluenceDiagramResponse(vertices=issues_list, edges=edges) + influence_diagram = InfluenceDiagramResponse(vertices=issues_list, edges=arcs) return influence_diagram def create_decision_tree(self, project_uuid: str) -> DecisionTreeResponse: @@ -70,11 +70,11 @@ def create_decision_tree(self, project_uuid: str) -> DecisionTreeResponse: Returns DecisionTreeResponse: Dict of vertices """ - influence_diagram = self.read_influence_diagram(project_uuid=project_uuid) - local_id = InfluenceDiagram.from_db(influence_diagram) - # local_id.to_json("id.json") - local_dt = local_id.convert_to_decision_tree() - # dt_json = json.loads(local_dt.to_json("dt.json")) - dt_json = json.loads(local_dt.to_json()) + influence_diagram = self.read_influence_diagram( + project_uuid=project_uuid + ).model_dump() + local_id = InfluenceDiagramConversion().from_json(influence_diagram) + local_dt = InfluenceDiagramToDecisionTree().conversion(local_id) + dt_json = DecisionTreeConversion().to_json(local_dt) decision_tree = DecisionTreeResponse.model_validate(dt_json) return decision_tree diff --git a/api/src/v0/services/structure_utils/decision_diagrams/decision_tree.py b/api/src/v0/services/structure_utils/decision_diagrams/decision_tree.py deleted file mode 100644 index 00b0fd0..0000000 --- a/api/src/v0/services/structure_utils/decision_diagrams/decision_tree.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Module defining the DecisionTree class - -A DecisionTree is a sub-class of ProbabilisticGraphModel. - - Raises: - RootNodeNotFound: When trying to define a decision tree without giving the root - node - -""" - -from __future__ import annotations - -import json -import logging -from typing import TYPE_CHECKING - -import networkx as nx - -from ..decision_diagrams.probabilistic_graph_model import ProbabilisticGraphModelABC - -if TYPE_CHECKING: # pragma: no cover - from ..decision_diagrams.node import NodeABC - - -logger = logging.getLogger(__name__) - - -class RootNodeNotFound(Exception): - def __init__(self): - error_message = "Decision tree has no defined root node" - super().__init__(error_message) - logger.critical(error_message) - - -class DecisionTree(ProbabilisticGraphModelABC): - """Decision tree class""" - - def __init__(self, *args, **kwargs): - """Create an instance of a DecisionTree - It is a wrapper around networkx.Digraph - - Args: - *args, **kwargs: arguments for networkx.DiGraph - - Attributes: - nx (networkx.DiGraph): networkx.DiGraph object containing the decision tree - (nodes and arcs) - """ - super().__init__(*args, **kwargs) - self.root = kwargs.get("root", None) - if self.root is not None: - self.nx.add_node(self.root) - - @classmethod - def initialize_diagram(cls, data: dict): - """Initialize a DecisionTree from data - - It looks for the node without parent for setting it - first in the list of nodes - - Args: - data (Dict): dictionary describing the graph model - {"nodes": List[NodeABC], "edges": List[Edge]} - - Only the 'edges' attribute is actually used. - - Returns: - DecisionTree: a new DecisionTree instance - - Example: - >>> n0 = UncertaintyNode("Uncertainty node 0", "u0") - >>> n1 = UncertaintyNode("Uncertainty node 1", "u1") - >>> n2 = UncertaintyNode("Uncertainty node 2", "u2") - >>> n3 = DecisionNode("Decision node 0", "d0") - >>> n4 = DecisionNode("Decision node 1", "d1") - >>> n5 = DecisionNode("Decision node 2", "d2") - >>> e0 = Edge(n0, n1, name="e0") - >>> e1 = Edge(n0, n2, name="e1") - >>> e2 = Edge(n1, n3, name="e2") - >>> e3 = Edge(n1, n4, name="e3") - >>> e4 = Edge(n2, n5, name="e2") - >>> graph = {"nodes": [n0, n1, n2, n3, n4, n5], \ - ... "edges": [e0, e1, e2, e3, e4],} - >>> DecisionTree.initialize_diagram(graph) - """ - roots = [] - g = nx.DiGraph([(arc.endpoint_start, arc.endpoint_end) for arc in data["edges"]]) - roots = [node for node in g.nodes if not list(g.predecessors(node))] - if not len(roots) == 1: - raise RootNodeNotFound - return cls(root=roots[0]) - - def set_root(self, root: NodeABC): - """set the root to a DecisionTree when this one has not been given - Typically, used if an empty decision tree is first generated and - then a node is given as its root. - - Args: - root (NodeABC): the root node - """ - self.root = root - if not self.nx.has_node(root): - self.add_node(root) - - def parent(self, node: NodeABC) -> NodeABC | None: - """return the parent node in the Decition Tree - - In a Decision Tree, a node has a unique parent - (The root doesn't have any) - - Args: - node (NodeABC): node to find the parent of - - Returns - Union[NodeABC, None]: the parent node or None is no parent node has been - found - """ - parents = self.get_parents(node) - return parents[0] if len(parents) > 0 else None - - def _to_json_stream(self) -> dict: - """convert the decision tree instance into a dictionary - It uses the method `networkx.readwrite.json_graph.tree_data()` - - Raises: - RootNodeNotFound: Raised when no root has been set in the decision tree - - Returns: - Dict: a representation of the tree - """ - - def propagate_branch_name(self, node, names): - predecessor = self.parent(node) - if not predecessor: - return "" - n = names[(predecessor, node)] - return n if isinstance(n, str) else "-".join(n) - - if self.root is None: - raise RootNodeNotFound - - edges_name = nx.get_edge_attributes(self.nx, "name") - tg = nx.readwrite.json_graph.tree_data(self.nx, self.root) - json_object = json.dumps( - tg, - default=lambda o: { - **o.to_dict(), - **{"branch_name": propagate_branch_name(self, o, edges_name)}, - }, - indent=2, - ) - return json_object diff --git a/api/src/v0/services/structure_utils/decision_diagrams/edge.py b/api/src/v0/services/structure_utils/decision_diagrams/edge.py deleted file mode 100644 index 395c7dd..0000000 --- a/api/src/v0/services/structure_utils/decision_diagrams/edge.py +++ /dev/null @@ -1,197 +0,0 @@ -"""Edges classes - -Raises: - EndpointTypeError: When endpoint cannot be set to start or end - UtilityNodeSuccessorError: When a UtilityNode has a successor which is not another - UtilityNode -""" - -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING - -from ..decision_diagrams.node import DecisionNode, UncertaintyNode, UtilityNode - -if TYPE_CHECKING: # pragma: no cover - from ....models.edge import EdgeResponse - from ..decision_diagrams.node import NodeABC - from ..decision_diagrams.probabilistic_graph_model import ProbabilisticGraphModelABC - - -logger = logging.getLogger(__name__) - - -class EndpointTypeError(Exception): - def __init__(self, mode): - self.mode = mode - error_message = f"endpoint cannot be set to mode {mode}" - super().__init__(error_message) - logger.critical(error_message) - - -class UtilityNodeSuccessorError(Exception): - def __init__(self): - error_message = "utility node can only have other utility nodes as successor" - super().__init__(error_message) - logger.critical(error_message) - - -class Edge: - """Class of nodes""" - - def __init__( - self, - endpoint_start: NodeABC, - endpoint_end: NodeABC, - name: str = None, - **kwargs, - ): - """Instance of Edge - - Args: - endpoint_start (NodeABC): start node of the edge - endpoint_end (NodeABC): end node of the edge - name (str, optional): name of the edge. Defaults to None. - - Private attributes: - _endpoint_start (NodeABC): start node of the edge - _endpoint_end (NodeABC): end node of the edge - _name (str, optional): name of the edge. Defaults to None. - _arc_type (str): type of the arc/edge, depending on the head node - - """ - self._endpoint_start = endpoint_start - self._endpoint_end = endpoint_end - self._name = name - self._arc_type = None - self._set_arc_type() - - @classmethod - def from_dict(cls, edge: dict, pgm: ProbabilisticGraphModelABC): - """Create an Edge instance from dictionary information - - endpoint nodes need to have already been added to the graph - - Args - edge (Dict): description of the edage as dictionary. - 'from' the uuid of the start endpoint of the edge (tail) - 'to' the uuid of the end endpoint of the edge (head) - 'name' the name of the edge - pgm (ProbabilisticGraphModel): the diagram into which the edge is added. - This is only used to get the nodes from - their uuid's. - - Returns: - Edge: a new edge instance is created. - - :warning: The Edge is not added to the diagram !!! - """ - endpoint_start = pgm.get_node_from_uuid(edge["from"]) - endpoint_end = pgm.get_node_from_uuid(edge["to"]) - attributes = {k: v for k, v in edge.items() if k not in ["from", "to", "id"]} - return cls(endpoint_start, endpoint_end, attributes.get("name", None)) - - @property - def endpoint_start(self) -> NodeABC: - """ - Returns: - NodeABC: the node at start of the edge (tail) - """ - return self._endpoint_start - - @property - def endpoint_end(self) -> NodeABC: - """ - Returns: - NodeABC: the node at end of the edge (head) - """ - return self._endpoint_end - - @property - def name(self) -> str: - """ - Returns: - str: the name of the edge - """ - return self._name - - def copy(self): - """copy an edge - - Returns: - Edge: copied edge, endpoints keeping their uuids - """ - # need to create the edge and then fill it so that - # the endpoints are the same including uuid - edge = Edge(None, None, self.name) - edge.set_endpoint(self.endpoint_start, mode="start") - edge.set_endpoint(self.endpoint_end, mode="end") - return edge - - def _set_arc_type(self): - """set (if possible) the type of the arc/edge (informational/conditional)""" - if isinstance(self._endpoint_end, DecisionNode): - self._arc_type = "informational" - elif isinstance(self._endpoint_end, UncertaintyNode): - self._arc_type = "conditional" - elif isinstance(self._endpoint_end, UtilityNode): - self._arc_type = "functional" - - def set_endpoint(self, node: NodeABC, mode="end"): - """Add an endpoint to an edge - - When creating an Edge, it does not need to have endpoints. Those endpoints - can be added at a later stage. This is actually necessary when converting an - influence diagram to a decision tree. - - Args: - node (NodeABC): node to add as an endpoint - mode (str, optional): where to add the node ['start'|'end']. - Defaults to "end". - - Raises: - EndpointTypeError: _description_ - UtilityNodeSuccessorError: _description_ - """ - if mode == "end": - self._endpoint_end = node - elif mode == "start": - self._endpoint_start = node - else: - raise EndpointTypeError(mode) - if mode == "end" and self._endpoint_start.is_utility_node: - if not node.is_utility_node: - raise UtilityNodeSuccessorError - - self._set_arc_type() - - def to_nx(self) -> tuple[tuple[NodeABC, NodeABC], dict]: - """convert the edge to a format compatible with the networkx format - - Returns: - Tuple[Tuple[NodeABC, NodeABC], Dict]: The Tuple[NodeABC, NodeABC] represents - the edge itself, and the Dictionary - adds extra information to the edge. - """ - return (self._endpoint_start, self._endpoint_end), { - "arc_type": self._arc_type, - "name": self._name, - } - - @classmethod - def from_db(cls, response: EdgeResponse, nodes: list[NodeABC]): - """create an Edges defined as an EdgeResponse and given a list of NodeABC - - Args: - response (EdgeResponse): response from DataBase defining the edge - nodes (list[NodeABC]): nodes existing in the diagram - - Returns - Edge: An Edge between existing nodes (NodeABC) - """ - uuid_tail = response.outV - uuid_head = response.inV - tail = [node for node in nodes if node.uuid == uuid_tail][0] - head = [node for node in nodes if node.uuid == uuid_head][0] - return Edge(tail, head, **response.__dict__) diff --git a/api/src/v0/services/structure_utils/decision_diagrams/influence_diagram.py b/api/src/v0/services/structure_utils/decision_diagrams/influence_diagram.py deleted file mode 100644 index 3d6406f..0000000 --- a/api/src/v0/services/structure_utils/decision_diagrams/influence_diagram.py +++ /dev/null @@ -1,402 +0,0 @@ -"""Module defining the InfluenceDiagram class - -An InfluenceDiagram is a sub-class of ProbabilisticGraphModel. - - Raises: - PartialOrderOutputModeError: When input to the partial_order method is wrong - -""" - -from __future__ import annotations - -import json -import logging -from typing import TYPE_CHECKING - -import networkx as nx -import pyAgrum as gum - -from ..decision_diagrams.decision_tree import DecisionTree -from ..decision_diagrams.edge import Edge -from ..decision_diagrams.node import DecisionNode, NodeABC, UncertaintyNode, UtilityNode -from ..decision_diagrams.probabilistic_graph_model import ProbabilisticGraphModelABC - -if TYPE_CHECKING: # pragma: no cover - from ....models.structure import InfluenceDiagramResponse - - -logger = logging.getLogger(__name__) - - -class PartialOrderOutputModeError(Exception): - def __init__(self, mode): - self.mode = mode - error_message = ( - f"output mode should be [view|copy] and have been entered as {mode}" - ) - super().__init__(error_message) - logger.critical(error_message) - - -class InfluenceDiagramNotAcyclicError(Exception): - def __init__(self): - error_message = "the influence diagram is not acyclic." - super().__init__(error_message) - logger.critical(error_message) - - -class ProbabilityFormatError(Exception): - def __init__(self, error): - self.mode = error - error_message = ( - f"Input probability cannot be used in pyagrum with error: {error}" - ) - super().__init__(error_message) - logger.critical(error_message) - - -class ArcFormatError(Exception): - def __init__(self, error): - self.mode = error - error_message = f"Input arc cannot be used in pyagrum with error: {error}" - super().__init__(error_message) - logger.critical(error_message) - - -class InfluenceDiagram(ProbabilisticGraphModelABC): - """Influence Diagram""" - - def __init__(self, *args, **kwargs): - """ - Create an instance of an InfluenceDiagram - It is a wrapper around networkx.Digraph - - Args: - *args, **kwargs: arguments for networkx.DiGraph - - Attributes: - nx (networkx.DiGraph): networkx.DiGraph object containing - the influence diagram (nodes and arcs) - """ - super().__init__(*args, **kwargs) - - @classmethod - def initialize_diagram(cls, data: dict): - """initialize an influence diagram given nodes and edges in a dictionary - - Args: - data (Dict): dictionary describing the graph model - {"nodes": List[NodeABC], "edges": List[Edge]} - - Example: - >>> n0 = UncertaintyNode("Uncertainty node 0", "u0") - >>> n1 = UncertaintyNode("Uncertainty node 1", "u1") - >>> n2 = DecisionNode("Decision node 0", "d0") - >>> n3 = UncertaintyNode("Uncertainty node 2", "u2") - >>> e0 = Edge(n0, n2, name="e0") - >>> e1 = Edge(n1, n2, name="e1") - >>> e2 = Edge(n2, n3, name="e2") - >>> graph = {"nodes": [n0, n1, n2, n3], "edges": [e0, e1, e2],} - >>> InfluenceDiagram.initialize_diagram(graph) - """ - return cls() - - def get_decision_nodes(self) -> list[DecisionNode]: - """Return a list of decision nodes - - Returns: - List[DecisionNode] - """ - return self._get_nodes_from_type("DecisionNode") - - def get_uncertainty_nodes(self) -> list[UncertaintyNode]: - """Return a list of uncertainty nodes - - Returns: - List[UncertaintyNode] - """ - return self._get_nodes_from_type("UncertaintyNode") - - def get_utility_nodes(self) -> list[UtilityNode]: - """Return a list of utility nodes - - Returns: - List[UtilityNode] - """ - return self._get_nodes_from_type("UtilityNode") - - @property - def decision_count(self) -> int: - """Return the number of decision nodes - - Returns: - int: the number of DecisionNode objects in the influence diagram - """ - return len(self.get_decision_nodes()) - - @property - def uncertainty_count(self) -> int: - """Return the number of uncertainty nodes - - Returns: - int: the number of UncertaintyNode objects in the influence diagram - """ - return len(self.get_uncertainty_nodes()) - - @property - def utility_count(self) -> int: - """Return the number of utility nodes - - Returns: - int: the number of UtilityNode objects in the influence diagram - """ - return len(self.get_utility_nodes()) - - def _to_json_stream(self) -> dict: - """convert the influence diagram instance into a dictionary - It uses the method `networkx.node_link_data()` - - Returns: - Dict: a representation of the diagram - """ - data = nx.node_link_data(self.nx, source="from", target="to", link="edges") - data["edges"] = [ - {k: v if not isinstance(v, NodeABC) else v.uuid for k, v in e.items()} - for e in data["edges"] - ] - json_object = json.dumps(data, default=lambda o: o.to_dict(), indent=4) - return json_object - - @classmethod - def from_db(cls, response: InfluenceDiagramResponse): - """Create a diagram from the DataBase Response - - Args: - response (InfluenceDiagramResponse): The influence diagram as read from - the database - - Returns: - ProbabilisticGraphModelABC: the decision diagram - - Example: - Assume the database returns an influence diagram described in the - `json_stream` - - >>> influence_diagram_response = \ - ... InfluenceDiagramResponse(vertices=json_stream['vertices'], \ - ... edges=json_stream['edges']) - >>> InfluenceDiagram.from_db(influence_diagram_response) - """ - nodes = [NodeABC.from_db(vertex) for vertex in response.vertices] - arcs = [Edge.from_db(edge, nodes) for edge in response.edges] - return cls.from_dict({"nodes": nodes, "edges": arcs}) - - def decision_elimination_order(self) -> list[NodeABC]: - """Decision Elimination Order algorithm - - Returns: - List[NodeABC] : the decision elimination order graph associated to - the influence diagram. Nodes in the list are copies of the - nodes of the influence diagram ones. - - TODO: add description of what is the algorithm about - """ - cid_copy = self.copy() - decisions = [] - decisions_count = cid_copy.decision_count - while decisions_count > 0: - nodes = list(cid_copy.nx.nodes()) - for node in nodes: - if not cid_copy.has_children(node): - if node.is_decision_node: - decisions.append(node) - decisions_count -= 1 - cid_copy.nx.remove_node(node) - return decisions - - def calculate_partial_order(self, mode="view") -> list[NodeABC]: - """Partial order algorithm - - - Args: - mode (str): ["view"(default)|"copy"] - returns a view or a copy of the nodes - - Returns - List[NodeABC]: list of nodes (copies or vioews) sorted in decision order - - TODO: add description of what the algorithm is about - TODO: handle utility nodes - """ - if mode not in ["view", "copy"]: - raise PartialOrderOutputModeError(mode) - - # get all chance nodes - uncertainty_node = self.get_uncertainty_nodes() - elimination_order = self.decision_elimination_order() - # TODO: Add utility nodes - partial_order = [] - - while elimination_order: - decision = elimination_order.pop() - parent_decision_nodes = [] - for parent in self.get_parents(decision): - if not parent.is_decision_node: - if parent in uncertainty_node: - parent_decision_nodes.append(parent) - uncertainty_node.remove(parent) - - if len(parent_decision_nodes) > 0: - partial_order += parent_decision_nodes - partial_order.append(decision) - - partial_order += uncertainty_node - - if mode == "copy": - partial_order = [node.copy() for node in partial_order] - - return partial_order - - def _output_branches_from_node( - self, node: NodeABC, node_in_partial_order: NodeABC, flip=True - ) -> list[tuple[Edge, NodeABC]]: - """Make a list of output branches from a node - - This method actually returns the states of the nodes. - - Args - node (NodeABC): node to find the output branch from - node_in_partial_order (NodeABC): associated node in the partial order - to - keep reference too - flip (bool): if True (default), flip the list of branches so a generated - decision tree is in the same order as the entered states. - If False, the tree will be flipped horizontally. - - Returns - List: the list of tuples (Edges, Node in partial order) - The edges have the input node as start endpoint and name given by the - state - """ - if node.is_utility_node: - tree_stack = [Edge(node, None, name=utility) for utility in node.utility] - if node.is_decision_node: - tree_stack = [ - Edge(node, None, name=alternative) for alternative in node.alternatives - ] - if node.is_uncertainty_node: - # This needs to be re-written according to the way we deal with probabilities - tree_stack = [Edge(node, None, name=outcome) for outcome in node.outcomes] - if flip: - tree_stack.reverse() - - return zip(tree_stack, [node_in_partial_order] * len(tree_stack), strict=False) - - def convert_to_decision_tree(self) -> DecisionTree: - """Convert the influence diagram into a DecisionTree object - - Returns: - DecisionTree: The symmetric decision tree equivalent to the influence diagram - - TODO: Update ID2DT according to way we deal with probabilities - """ - partial_order = self.calculate_partial_order() - root_node = partial_order[0] - # decision_tree = DecisionTree.initialize_with_root(root_node) - decision_tree = DecisionTree(root=root_node) - # tree_stack contains views of the partial order nodes - # decision_tree contains copy of the nodes (as they appear several times) - tree_stack = [(root_node, root_node)] - - while tree_stack: - element = tree_stack.pop() - - if isinstance(element[0], NodeABC): - tree_stack += self._output_branches_from_node(*element) - - else: # element is a branch - endpoint_start_index = partial_order.index(element[1]) - - if endpoint_start_index < len(partial_order) - 1: - endpoint_end = partial_order[endpoint_start_index + 1].copy() - tree_stack.append( - (endpoint_end, partial_order[endpoint_start_index + 1]) - ) - else: - # endpoint_end = UtilityNode( - # name=element[0].name, tag=element[0].name.lower() - # ) - endpoint_end = UtilityNode(shortname="ut", description="Utility") - - element[0].set_endpoint(endpoint_end) - decision_tree.add_edge( - element[0] - ) # node is added when the branch is added - - return decision_tree - - @staticmethod - def _nodes_to_pyagrum(nodes, gum_id): - # create an uuid for gum as 8 bytes integer and keep relation to uuid - uuid_dot_to_gum = {} - uuid_gum_to_dot = {} - node_uuid = {} - - for node in nodes: - labelized_variables = [node.shortname, node.description] - if isinstance(node, UncertaintyNode): - try: - labelized_variables.append(node.outcomes) - variable_id = gum_id.addChanceNode( - gum.LabelizedVariable(*labelized_variables) - ) - except Exception as e: - raise ProbabilityFormatError(e) - elif isinstance(node, DecisionNode): - # This works even when alternatives are [""] or None - labelized_variables.append(node.alternatives) - variable_id = gum_id.addDecisionNode( - gum.LabelizedVariable(*labelized_variables) - ) - elif isinstance(node, UtilityNode): - # Utility not yet implemented - labelized_variables.append(1) - variable_id = gum_id.addUtilityNode( - gum.LabelizedVariable(*labelized_variables) - ) - - uuid_dot_to_gum[node.uuid] = variable_id - uuid_gum_to_dot[variable_id] = node.uuid - node_uuid[variable_id] = node - - return uuid_dot_to_gum, uuid_gum_to_dot, node_uuid - - @staticmethod - def _arcs_to_pyagrum(edges, gum_id, uuid_dot_to_gum): - for arc in edges: - tail = uuid_dot_to_gum[arc[0].uuid] - head = uuid_dot_to_gum[arc[1].uuid] - try: - gum_id.addArc(tail, head) - except Exception as e: - raise ArcFormatError(e) - return None - - def to_pyagrum(self): - if not nx.is_directed_acyclic_graph(self.nx): - raise InfluenceDiagramNotAcyclicError - - gum_id = gum.InfluenceDiagram() - variable_id = [] - - uuid_dot_to_gum, uuid_gum_to_dot, node_uuid = InfluenceDiagram._nodes_to_pyagrum( - self.nx.nodes, gum_id - ) - # Add head and tail in gum_id - InfluenceDiagram._arcs_to_pyagrum(self.nx.edges, gum_id, uuid_dot_to_gum) - - for variable_id in uuid_gum_to_dot: - if isinstance(node_uuid[variable_id], UncertaintyNode): - for agrum_prob in node_uuid[variable_id].probabilities.to_pyagrum(): - gum_id.cpt(variable_id)[agrum_prob[0]] = agrum_prob[1] - - return gum_id diff --git a/api/src/v0/services/structure_utils/decision_diagrams/node.py b/api/src/v0/services/structure_utils/decision_diagrams/node.py deleted file mode 100644 index 74d4ec7..0000000 --- a/api/src/v0/services/structure_utils/decision_diagrams/node.py +++ /dev/null @@ -1,467 +0,0 @@ -"""Nodes classes - -NodeABC is an abstract class with 3 concretizations: -- DecisionNode -- UncertaintyNode -- UtilityNode - - Raises: - NodeTypeError: When failing to create an instance of a node -""" - -from __future__ import annotations - -import ast -import logging -import re -from abc import ABC, abstractmethod -from importlib import import_module -from typing import TYPE_CHECKING -from uuid import uuid4 - -from ..probability.discrete_conditional_probability import DiscreteConditionalProbability -from ..probability.discrete_unconditional_probability import ( - DiscreteUnconditionalProbability, -) - -if TYPE_CHECKING: # pragma: no cover - from ....models.issue import IssueResponse - from ..probability.abstract_probability import ProbabilityABC - - -logger = logging.getLogger(__name__) - - -class NodeTypeError(Exception): - def __init__(self, node_type): - error_message = f"failing instantiation of {node_type}" - super().__init__(error_message) - logger.critical(error_message) - - -class NodeABC(ABC): - """Abstract class of nodes""" - - def __init__(self, shortname: str, description: str, unique_id=None): - """instantiation of an (abstract) node - - Args: - description (str): description of the node - shortname (str): shortname of the node - unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - - Private attributes: - _description (str): description of the node - _shortname (str): shortname of the node - _unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - """ - self._shortname = shortname - self._description = description - self._uuid = unique_id if unique_id is not None else str(uuid4()) - - @property - def description(self) -> str: - """Return the `description` attribute - - Returns: - str: description of the node - """ - return self._description - - @property - def shortname(self) -> str: - """Return the `shortname` attribute - - Returns: - str: shortname of the node - """ - return self._shortname - - @property - def uuid(self) -> str: - """Return the `uuid` attribute - - Returns: - str: uuid of the node - """ - return self._uuid - - @property - def is_decision_node(self) -> bool: - """ - Returns: - bool: True of the node is a DecisionNode, false otherwise - """ - return isinstance(self, DecisionNode) - - @property - def is_uncertainty_node(self) -> bool: - """ - Returns: - bool: True of the node is a UncertaintyNode, false otherwise - """ - return isinstance(self, UncertaintyNode) - - @property - def is_utility_node(self) -> bool: - """ - Returns: - bool: True of the node is a UtilityNode, false otherwise - """ - return isinstance(self, UtilityNode) - - @property - @abstractmethod - def states(self): - raise NotImplementedError - - def copy(self): - """Copy of the node - - Returns: - NodeABC: a copy of the node, with a new uuid - """ - shortname = self.shortname - description = self.description - copied_node = type(self)(shortname, description) - for attribute, value in self.__dict__.items(): - if attribute not in ["_shortname", "_description", "_uuid"]: - setattr(copied_node, attribute, value) - return copied_node - - def to_dict(self) -> dict: - """Convert the Node object into a dictionary - - Each property becomes the key of a dictionary. - Private and restricted attributes loose their underscores - - Returns: - Dict: a dictionary representing the node - """ - d = {"node_type": self.__class__.__name__} - properties = self.__dict__ - for property in properties: - key = property.lstrip("_") - if hasattr(getattr(self, key, None), "to_dict"): - d[key] = getattr(self, key).to_dict() - else: - d[key] = getattr(self, key, None) - return d - - @staticmethod - @abstractmethod - def get_instance_input(issue): - raise NotImplementedError - - @staticmethod - def from_dict(data: dict) -> NodeABC: - """Create a Node from data in a dictionary - - Args - data (Dict): dictionary describing the node - keys are: - 'node_type': the name of the Node instance as a string - relevant keys according to the type of node - - Returns - NodeABC: the created concrete type of Node - - Example: - >>> node = { - ... 'node_type': 'DecisionNode', - ... 'description': "a decision", - ... 'shortname': "D" - ... } - >>> Node.from_dict(node) - """ - node_type = data.pop("node_type") - try: - node = globals()[node_type](**data) - except Exception: - raise NodeTypeError(node_type) - return node - - @staticmethod - def from_db(response: IssueResponse) -> NodeABC: - """Create a node from DataBase data - - Args: - response (IssueResponse): response describing the node - attributes are: - 'category': the name of the Node instance as a string - relevant attributes according to the type of node - - Returns - NodeABC: the created concrete type of Node - """ - node_type = response.category - node_type = "Utility" if node_type == "Value Metric" else node_type - node_type += "Node" - try: - issue_data = globals()[node_type].get_instance_input(response) - except Exception: - raise NodeTypeError(node_type) - - issue_data_parsed = {} - for k, v in issue_data.items(): - try: - v = ast.literal_eval(v) - except Exception as exc: - e = exc - finally: - if e: - pass # trick for passing ruff and bandit... - issue_data_parsed[k] = v - node = globals()[node_type]._from_db_model(**issue_data_parsed) - return node - - @classmethod - @abstractmethod - def _from_db_model(cls, **kwargs): - raise NotImplementedError - - -class DecisionNode(NodeABC): - """Decision node class""" - - def __init__( - self, - shortname: str, - description: str, - uuid=None, - alternatives=None, - **kwargs, - ): - """Create an instance of a DecisionNode - - Args: - description (str): description of the node - shortname (str): shortname of the node - unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - alternatives (List[str], optional): alternatives (states) of the decision. - Defaults to None. - - Private attributes: - _description (str): description of the node - _shortname (str): shortname of the node - _unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - _alternatives (List[str], optional): alternatives (states) of the decision. - Defaults to None. - """ - super().__init__(shortname, description, uuid) - self._alternatives = alternatives - - @staticmethod - def get_instance_input(issue: IssueResponse) -> dict: - """Get the relevant input for the instantiation - - Args: - issue (IssueResponse): issue from the DataBase - - Returns: - Dict: keys/values of the DataBase issue relevant for the instantiation - """ - input_list = ["description", "shortname", "uuid", "alternatives"] - return {key: getattr(issue, key) for key in input_list} - - @property - def states(self) -> list[str]: - """ - Returns: - List[str]: the alternatives (states) of the decision - """ - return self.alternatives - - @property - def alternatives(self) -> list[str]: - """ - Returns: - List[str]: the alternatives (states) of the decision - """ - return self._alternatives if isinstance(self._alternatives, list) else [] - - @classmethod - def _from_db_model(cls, **kwargs): - """instantiation from the DataBase Model - - Args: - kwargs: keys/values of the DataBase issue relevant for the instantiation - """ - return cls(**kwargs) - - -class UncertaintyNode(NodeABC): - """Uncertainty (or chance) node class""" - - def __init__( - self, - shortname: str, - description: str, - uuid=None, - probabilities: ProbabilityABC = None, - **kwargs, - ): - """Create an instance of a UncertaintyNode - - Args: - description (str): description of the node - shortname (str): shortname of the node - unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - probabilities (ProbabilityABC, optional): probability associated to the node. - Defaults to None. - - Private attributes: - _description (str): description of the node - _shortname (str): shortname of the node - _unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - _probabilities (ProbabilityABC, optional): probability associated to the - node. Defaults to None. - """ - super().__init__(shortname, description, uuid) - self._probabilities = probabilities - - @staticmethod - def get_instance_input(issue: IssueResponse) -> dict: - """Get the relevant input for the instantiation - - Args: - issue (IssueResponse): issue from the DataBase - - Returns: - Dict: keys/values of the DataBase issue relevant for the instantiation - """ - input_list = ["description", "shortname", "uuid", "probabilities"] - return {key: getattr(issue, key) for key in input_list} - - @property - def probabilities(self) -> ProbabilityABC: - """ - Returns: - ProbabilityABC: the probabibility associated to the node - """ - return self._probabilities - - @property - def states(self) -> list[str]: - """ - Returns: - List[str]: the outcomes (states) of the uncertainty - """ - return self.outcomes - - @property - def outcomes(self) -> list[str]: - """ - Returns: - List[str]: the outcomes (states) of the uncertainty - """ - if not isinstance( - self._probabilities, DiscreteConditionalProbability - ) and not isinstance(self._probabilities, DiscreteUnconditionalProbability): - return () - return self._probabilities.outcomes - - @classmethod - def _from_db_model(cls, **kwargs): - """instantiation from the DataBase Model - - Args: - kwargs: keys/values of the DataBase issue relevant for the instantiation - """ - if kwargs["probabilities"] is not None: - ptype = kwargs["probabilities"].dtype - module_name = ( - "src.v0.services.structure_utils.probability." - + re.sub(r"([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", r"\1 ", ptype) - .replace(" ", "_") - .lower() - ) - kwargs["probabilities"] = getattr( - import_module(module_name), ptype - ).from_db_model(kwargs["probabilities"]) - return cls(**kwargs) - - -class UtilityNode(NodeABC): - """Utility (or value) node class""" - - def __init__( - self, - shortname: str, - description: str, - uuid=None, - utility=None, - **kwargs, - ): - """Create an instance of a UtilityNode - - Args: - description (str): description of the node - shortname (str): shortname of the node - unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - utility (List[str], optional): utility associated to the node. Defaults to - None. - - Private attributes: - _description (str): description of the node - _shortname (str): shortname of the node - _unique_id (str, optional): uuid. Defaults to None. If None, the uuid is - attributed at instantiation. - _utility (List[str], optional): utility associated to the node. Defaults to - None. - - TODO: implement utility matrix attribute through a Utility class. So far it is - rather a place holder. - """ - super().__init__(shortname, description, uuid) - self._utility = utility # The value is typically E[gain] but we could minimize - # the risk instead of maximizing the gain - - @staticmethod - def get_instance_input(issue: IssueResponse) -> dict: - """Get the relevant input for the instantiation - - Args: - issue (IssueResponse): issue from the DataBase - - Returns: - Dict: keys/values of the DataBase issue relevant for the instantiation - """ - input_list = [ - "description", - "shortname", - "uuid", - ] # TODO: Add utility field to DB - return {key: getattr(issue, key) for key in input_list} - - @property - def states(self) -> list[str]: - """ - Returns: - List[str]: the utility entries (states) of the utility - """ - return self.utility - - @property - def utility(self) -> list[str]: - """ - Returns: - List[str]: the utility entries (states) of the utility - """ - return self._utility if isinstance(self._utility, list) else [] - - @classmethod - def _from_db_model(cls, **kwargs): - """instantiation from the DataBase Model - - Args: - kwargs: keys/values of the DataBase issue relevant for the instantiation - """ - return cls(**kwargs) diff --git a/api/src/v0/services/structure_utils/decision_diagrams/probabilistic_graph_model.py b/api/src/v0/services/structure_utils/decision_diagrams/probabilistic_graph_model.py deleted file mode 100644 index c9c580c..0000000 --- a/api/src/v0/services/structure_utils/decision_diagrams/probabilistic_graph_model.py +++ /dev/null @@ -1,184 +0,0 @@ -"""Module defining the ProbabilisticGraphModel Abstract class""" - -from __future__ import annotations - -import importlib -import logging -from abc import ABC, abstractmethod -from pathlib import Path -from typing import TYPE_CHECKING - -import networkx as nx - -if TYPE_CHECKING: # pragma: no cover - from ..decision_diagrams.edge import Edge - from ..decision_diagrams.node import NodeABC - - -logger = logging.getLogger(__name__) - - -class ProbabilisticGraphModelABC(ABC): - NODES_MODULE_PATH = "src.v0.services.structure_utils.decision_diagrams.node" - - """Probabilistic Graph Model""" - - def __init__(self, *args, **kwargs): - """ - Create an instance of a ProbabilisticGraphModel (ABSTRACT class!!!) - It is a wrapper around networkx.Digraph - - Args: - *args, **kwargs: arguments for networkx.DiGraph - - Attributes: - nx: networkx object - """ - self.nx = nx.DiGraph(*args, **kwargs) - - @classmethod - def from_dict(cls, data: dict): - """Create a probabilistic graph model from data in a dictionary - - Args: - data (Dict): dictionary describing the graph model - {"nodes": List[NodeABC], "edges": List[Edge]} - - Returns: - ProbabilisticGraphModelABC: the decision diagram - """ - diagram = cls.initialize_diagram(data) - for node in data["nodes"]: - diagram.add_node(node) - for edge in data.get("edges", []): - diagram.add_edge(edge) - return diagram - - @classmethod - @abstractmethod - def initialize_diagram(cls, data): - """Initialize a diagram with data""" - raise NotImplementedError - - def add_node(self, node: NodeABC): - """Add a node to the graph - - Args: - node (NodeABC): node to be added - """ - self.nx.add_node(node) - - def add_edge(self, edge: Edge): - """Add an edge to the graph - - Args: - edge (Edge): Edge to be added. If some of the end points do not exist, they - are added to the graph too. - """ - nx_edge, nx_attributes = edge.to_nx() - self.nx.add_edge(nx_edge[0], nx_edge[1], **nx_attributes) - - def copy(self): - """copy the probabilistic graph model - - Returns - ProbabilisticGraphModel: a copy of the graph. uuid's of the nodes are copied - too. - """ - new_id = type(self)() # Need to instance from the concrete class - new_id.nx = self.nx.copy() - return new_id - - def get_parents(self, node: NodeABC) -> list[NodeABC]: - """get parents of a given node - - Args: - node (NodeABC): node to find the parents of - - Returns - List[NodeABC]: the list of the parents of the given node - """ - return list(self.nx.predecessors(node)) - - def get_children(self, node: NodeABC) -> list[NodeABC]: - """get children of a given node - - Args: - node (NodeABC): node to find the children of - - Returns - List[NodeABC]: the list of the children of the given node - """ - return list(self.nx.successors(node)) - - def get_node_type(self, node: NodeABC) -> str: - """Return the type of a given node - - Args: - node (NodeABC): node to examine - - Returns - str: a string describing the type of the node (class name in lower font and - without the "Node" suffix) - """ - return type(node).__name__.replace("Node", "").lower() - - def _get_nodes_from_type(self, node_type_string: str) -> list[NodeABC]: - """find all the nodes of a given type - - Args: - node_type_str (str): type of the nodes to find as a string - - Returns - List[NodeABC]: the list of the nodes of given type - """ - node_type = getattr( - importlib.import_module(self.NODES_MODULE_PATH), node_type_string - ) - node_list = [] - for node in list(self.nx.nodes(data=True)): - if isinstance(node[0], node_type): - node_list.append(node[0]) - return node_list - - def has_children(self, node: NodeABC) -> bool: - """Check for existence of children - - Args: - node (NodeABC): node to check for children - - Returns - bool: existence (True) or not (False) of children to the given node - """ - return len(self.get_children(node)) > 0 - - def to_json(self, filepath: Path = None) -> str: - """Convert the graph into a json object - - Args: - filepath (Path, optional): Path where to copy the json file. None if no - output file. - - Returns: - str: json object as a string - """ - json_object = self._to_json_stream() - if filepath: - with open(filepath, "w") as outfile: - outfile.write(json_object) - return json_object - - @abstractmethod - def _to_json_stream(self): - raise NotImplementedError - - def get_node_from_uuid(self, uuid: str) -> NodeABC: - """get a node given its uuid - - Args: - uuid (str): uuid of node to look for - - Returns: - NodeABC: node object having the given uuid - """ - return [node for node in self.nx if node.uuid == uuid][0] diff --git a/api/src/v0/services/structure_utils/probability/abstract_probability.py b/api/src/v0/services/structure_utils/probability/abstract_probability.py deleted file mode 100644 index f7cd486..0000000 --- a/api/src/v0/services/structure_utils/probability/abstract_probability.py +++ /dev/null @@ -1,56 +0,0 @@ -import logging -from abc import ABC, abstractmethod - -logger = logging.getLogger(__name__) - - -class ProbabilityABC(ABC): - """ProbabilityABC""" - - # def __init__(self): - # pass - - @classmethod - @abstractmethod - def from_db_model(cls, **data): - raise NotImplementedError - - @abstractmethod - def initialize_nan(self): - raise NotImplementedError - - @abstractmethod - def set_to_uniform(self): - raise NotImplementedError - - @abstractmethod - def normalize(self): - raise NotImplementedError - - @abstractmethod - def isnormalized(self): - raise NotImplementedError - - @classmethod - @abstractmethod - def from_json(cls): - raise NotImplementedError - - @abstractmethod - def to_json(self): - raise NotImplementedError - - def to_dict(self): - raise NotImplementedError - - @abstractmethod - def get_distribution(self, **kwargs): - raise NotImplementedError - - @abstractmethod - def to_pyagrum(self): - raise NotImplementedError - - @abstractmethod - def to_pycid(self): - raise NotImplementedError diff --git a/api/src/v0/services/structure_utils/probability/discrete_conditional_probability.py b/api/src/v0/services/structure_utils/probability/discrete_conditional_probability.py deleted file mode 100644 index a8ac3bc..0000000 --- a/api/src/v0/services/structure_utils/probability/discrete_conditional_probability.py +++ /dev/null @@ -1,183 +0,0 @@ -import json -import logging -import re -from itertools import product - -import numpy as np -import xarray as xr -from numpy.typing import ArrayLike - -from ..probability.abstract_probability import ProbabilityABC - -logger = logging.getLogger(__name__) - - -class VariableNot1D(Exception): - def __init__(self): - error_message = "One of the variables cannot be interpreted as 1D" - super().__init__(error_message) - logger.critical(error_message) - - -class CPTTypeError(Exception): - def __init__(self, cpt_type): - error_message = f"Expected DiscreteConditionalProbability dtype, got {cpt_type}" - super().__init__(error_message) - logger.critical(error_message) - - -class DiscreteConditionalProbability(ProbabilityABC): - """DiscreteConditionalProbability""" - - def __init__(self, probability_function: ArrayLike, variables: dict) -> None: - """ - Parameters - ----------- - probability_function: ArrayLike - gives the probability that a variable is equal to some value - variables: dict - involves the variable name and its values; values refers to the set of - possible outcomes - - Warning - -------- - Spaces in variable names and values will be removed. - - Notes - ----- - - For the Conditional Probability Table, P(Test Result | Test, State) such as - - || 1. Test || yes yes | no no - || 2. State || Peach | Lemon | Peach | Lemon - ----------------------------------------------------------------- - 0. Test Result || || | | | - ----------------------------------------------------------------- - no Test || || 0.1 | 0.05 | 0.85 | 0.46 - Peach || || 0.7 | 0.35 | 0.12 | 0.26 - Lemon || || 0.2 | 0.60 | 0.03 | 0.28 - - the probability_function will be - [ - [0.1, 0.05, 0.85, 0.46], - [0.7, 0.35, 0.12, 0.26], - [0.2, 0.60, 0.03, 0.28], - ] - - and the variables - { - "Test Result": ["no Test", "Peach", "Lemon"], - "Test": ["yes", "no"], - "State": ["Peach", "Lemon"], - } - """ - super().__init__() - if any( - max(np.asarray(v).shape) != np.asarray(v).size for v in variables.values() - ): - raise VariableNot1D - # remove white spaces as the string is used as variable name by xarray - variables = {re.sub(r"\s+", "", k): v for k, v in variables.items()} - self._cpt = xr.DataArray(probability_function, coords=variables) - - @property - def outcomes(self): - variable_name = self._cpt.dims[0] - return tuple(self._cpt.coords[variable_name].data.tolist()) - - @property - def variables(self): - return self._cpt.dims - - @classmethod - def from_db_model(cls, data): - array_size = tuple([len(v) for v in data.variables.values()]) - distribution = np.reshape(data.probability_function, array_size) - return cls(distribution, data.variables) - - def add_conditioning_variable(self, variables: dict): - raise NotImplementedError - - def remove_conditioning_variable(self, variables: dict): - raise NotImplementedError - - @classmethod - def initialize_nan(cls, variables: dict): - data_shape = tuple(np.asarray(v).shape[0] for v in variables.values()) - return cls(np.full(data_shape, np.nan), variables=variables) - - def set_to_uniform(self): - data_shape = tuple(np.asarray(v).shape[0] for v in self._cpt.coords.values()) - self._cpt.data = np.ones(data_shape) / data_shape[0] - return self - - def normalize(self): - self._cpt = self._cpt / np.apply_over_axes( - np.sum, self._cpt, range(1, self._cpt.ndim) - ) - return self - - def isnormalized(self, threshold=1e-6): - return np.all(np.linalg.norm(self._cpt.sum(axis=0) - 1.0) < threshold) - - @classmethod - def from_json(cls, json_str: str): - d = json.loads(json_str) - if d["dtype"] != cls.__name__: - raise CPTTypeError(d["dtype"]) - return cls(d["probability_function"], d["variables"]) - - def to_json(self): - return json.dumps(self.to_dict()) - - def to_dict(self): - d_ = self._cpt.to_dict() - d = { - "dtype": self.__class__.__name__, - "probability_function": d_["data"], - "variables": {k: v["data"] for k, v in d_["coords"].items()}, - } - return d - - def get_distribution(self, **variables): - """Return the probability distribution - - Parameters - ---------- - **variables: - variables (name=value) for which the distribution is desired - - Return - ------ - xr.DataArray - """ - return self._cpt.sel(**variables) - - # just for asymmetric purposes, i.e., the NA outcome - # symbolizing the asymmetry; otherwise, if outcome data - # has changed, just remake the cpt - def add_na_outcomes(self): - raise NotImplementedError - - def to_pyagrum(self): - # agrum = list() - coords = self._cpt.coords - variables = { - key: coords[key].data.tolist() for key in coords if key is not coords.dims[0] - } - agrum_dict = {k: list(range(len(v))) for k, v in variables.items()} - agrum_dict = [ - dict(zip(agrum_dict.keys(), values, strict=False)) - for values in product(*agrum_dict.values()) - ] - agrum_prob = [ - self.get_distribution( - **{key: coords[key][val] for key, val in item.items()} - ).data.tolist() - for item in agrum_dict - ] - agrum = list(zip(agrum_dict, agrum_prob, strict=False)) - return agrum - - def to_pycid(self): - raise NotImplementedError diff --git a/api/src/v0/services/structure_utils/probability/discrete_unconditional_probability.py b/api/src/v0/services/structure_utils/probability/discrete_unconditional_probability.py deleted file mode 100644 index 6f2c577..0000000 --- a/api/src/v0/services/structure_utils/probability/discrete_unconditional_probability.py +++ /dev/null @@ -1,188 +0,0 @@ -import json -import logging -import re -from itertools import product - -import numpy as np -import xarray as xr -from numpy.typing import ArrayLike - -from ..probability.abstract_probability import ProbabilityABC - -logger = logging.getLogger(__name__) - - -class VariableNot1D(Exception): - def __init__(self): - error_message = "One of the variables cannot be interpreted as 1D" - super().__init__(error_message) - logger.critical(error_message) - - -class CPTTypeError(Exception): - def __init__(self, cpt_type): - error_message = ( - f"Expected DiscreteUnconditionalProbability dtype, got {cpt_type}" - ) - super().__init__(error_message) - logger.critical(error_message) - - -class AgrumConversionError(Exception): - def __init__(self): - error_message = "pyAgrum only takes 1D variables in UncertaintyNode" - super().__init__(error_message) - logger.critical(error_message) - - -class DiscreteUnconditionalProbability(ProbabilityABC): - """DiscreteUnconditionalProbability""" - - def __init__(self, probability_function: ArrayLike, variables: dict) -> None: - """ - Parameters - ----------- - probability_function: ArrayLike - gives the probability that a variable is equal to some value - variables: dict - involves the variable name and its values; values refers to the set of - possible outcomes - - Warning - -------- - Spaces in variable names and values will be removed. - - Notes - ----- - - For the Probability P(A, B , C) such as - - || 1. B || b1 b1 | b2 b2 - || 2. C || c1 | c2 | c1 | c1 - ----------------------------------------------------------------- - 0. A || || | | | - ----------------------------------------------------------------- - a1 || || 0.00 | 0.13 | 0.20 | 0.06 - a2 || || 0.03 | 0.28 | 0.11 | 0.02 - a3 || || 0.12 | 0.04 | 0.10 | 0.01 - - the probability_function will be - [ - [0.00, 0.13, 0.20, 0.06], - [0.03, 0.18, 0.11, 0.02], - [0.12, 0.04, 0.10, 0.01], - ] - - and the variables - { - "A": ["a1", "a2", "a3"], - "B": ["b1", "b2"], - "C": ["c1", "c2"], - } - """ - super().__init__() - if any( - max(np.asarray(v).shape) != np.asarray(v).size for v in variables.values() - ): - raise VariableNot1D - variables = {re.sub(r"\s+", "", k): v for k, v in variables.items()} - self._cpt = xr.DataArray(probability_function, coords=variables) - - @property - def outcomes(self): - variable_names = self._cpt.dims - if len(variable_names) == 1: - return tuple(self._cpt.coords[variable_names[0]].data.tolist()) - else: - return tuple( - product( - *tuple( - tuple(self._cpt.coords[vn].data.tolist()) - for vn in variable_names - ) - ) - ) - - @property - def variables(self): - return self._cpt.dims - - @classmethod - def from_db_model(cls, data): - array_size = tuple([len(v) for v in data.variables.values()]) - distribution = np.reshape(data.probability_function, array_size) - return cls(distribution, data.variables) - - @classmethod - def initialize_nan(cls, variables: dict): - data_shape = tuple(np.asarray(v).shape[0] for v in variables.values()) - return cls(np.full(data_shape, np.nan), variables=variables) - - def set_to_uniform(self): - data_shape = tuple(np.asarray(v).shape[0] for v in self._cpt.coords.values()) - self._cpt.data = np.ones(data_shape) / np.prod(data_shape) - return self - - # does it even make sense to normalize? - def normalize(self): - # normalize columwise before normalizing wrt the whole matrix? - # self._cpt = np.apply_over_axes(np.sum, self._cpt, range(1, self._cpt.ndim)) - self._cpt = self._cpt / self._cpt.sum() - return self - - def isnormalized(self, threshold=1e-6): - return np.all(np.linalg.norm(self._cpt.sum() - 1.0) < threshold) - - @classmethod - def from_json(cls, json_str: str): - d = json.loads(json_str) - if d["dtype"] != cls.__name__: - raise CPTTypeError(d["dtype"]) - return cls(d["probability_function"], d["variables"]) - - def to_json(self): - return json.dumps(self.to_dict()) - - def to_dict(self): - d_ = self._cpt.to_dict() - pf = np.array(d_["data"]) - d = { - "dtype": self.__class__.__name__, - "probability_function": np.reshape(pf, (pf.shape[0], -1)).tolist(), - "variables": {k: v["data"] for k, v in d_["coords"].items()}, - } - return d - - def get_distribution(self, **variables): - """Return the probability distribution - - Parameters - ---------- - **variables: - variables (name=value) for which the distribution is desired - - Return - ------ - xr.DataArray - """ - return self._cpt.sel(**variables) - - # just for asymmetric purposes, i.e., the NA outcome - # symbolizing the asymmetry; otherwise, if outcome data - # has changed, just remake the cpt - def add_na_outcomes(self): - raise NotImplementedError - - def to_pyagrum(self): - variables = self.variables - if len(variables) != 1: - raise AgrumConversionError - return [ - ( - {}, - [self._cpt.sel(**{variables[0]: state}) for state in self.outcomes], - ) - ] - - def to_pycid(self): - raise NotImplementedError diff --git a/api/tests/v0/models/test_issue.py b/api/tests/v0/models/test_issue.py index 443da84..5cdc503 100644 --- a/api/tests/v0/models/test_issue.py +++ b/api/tests/v0/models/test_issue.py @@ -19,10 +19,10 @@ def test_CommentData_fail(): comment="a comment", ) assert ( - "1 validation error for CommentData\nauthor\n Field required [type=missing, " - "input_value={'comment': 'a comment'}, input_type=dict]\n For further " - "information visit https://errors.pydantic.dev/2.10/v/missing" in str(exc.value) - ) + "1 validation error for CommentData\nauthor\n Field required " + "[type=missing, input_value={'comment': 'a comment'}, input_type=dict]\n" + " For further information visit https://errors.pydantic.dev/2.11/v/missing" + ) in str(exc.value) def test_ProbabilityData_default(): diff --git a/api/tests/v0/repositories/test_issue.py b/api/tests/v0/repositories/test_issue.py index 18d3d0c..0a29b20 100644 --- a/api/tests/v0/repositories/test_issue.py +++ b/api/tests/v0/repositories/test_issue.py @@ -48,7 +48,7 @@ def issue(): "tag": ['["junk"]'], "index": ["1234"], "category": ["today"], - "keyUncertainty": ["True"], + "keyUncertainty": ["true"], "decisionType": ["Tactical"], "alternatives": ['["yes", "no"]'], "probabilities": [ diff --git a/api/tests/v0/routes/test_issue.py b/api/tests/v0/routes/test_issue.py index 846d359..37ee3e7 100644 --- a/api/tests/v0/routes/test_issue.py +++ b/api/tests/v0/routes/test_issue.py @@ -28,7 +28,7 @@ def issue(): "tag": ["junk"], "index": "1234", "category": "today", - "keyUncertainty": "True", + "keyUncertainty": "true", "decisionType": "Tactical", "alternatives": ["yes", "no"], "probabilities": { diff --git a/api/tests/v0/routes/test_structure.py b/api/tests/v0/routes/test_structure.py index 350a70a..c6a73ae 100644 --- a/api/tests/v0/routes/test_structure.py +++ b/api/tests/v0/routes/test_structure.py @@ -96,12 +96,12 @@ def graph(): def test_read_influence_diagram_success(mock_service, graph): - vertices = [ + nodes = [ IssueResponse.model_validate(graph[0]), IssueResponse.model_validate(graph[1]), IssueResponse.model_validate(graph[2]), ] - edges = [ + arcs = [ EdgeResponse( uuid="101", id="101", outV="11-aa", inV="22-bb", label="influences" ), @@ -110,7 +110,7 @@ def test_read_influence_diagram_success(mock_service, graph): ), ] mock_service.return_value.read_influence_diagram.return_value = ( - InfluenceDiagramResponse(vertices=vertices, edges=edges) + InfluenceDiagramResponse(vertices=nodes, edges=arcs) ) project_uuid = "0" response = client.get( diff --git a/api/tests/v0/services/structure_utils/test_probability/__init__.py b/api/tests/v0/services/analysis/__init__.py similarity index 100% rename from api/tests/v0/services/structure_utils/test_probability/__init__.py rename to api/tests/v0/services/analysis/__init__.py diff --git a/api/tests/v0/services/analysis/test_id_to_dt.py b/api/tests/v0/services/analysis/test_id_to_dt.py new file mode 100644 index 0000000..09ebc52 --- /dev/null +++ b/api/tests/v0/services/analysis/test_id_to_dt.py @@ -0,0 +1,773 @@ +import numpy as np +import pytest + +from src.v0.services.analysis.id_to_dt import InfluenceDiagramToDecisionTree +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.decision_tree import DecisionTree +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) +from src.v0.services.classes.influence_diagram import InfluenceDiagram +from src.v0.services.classes.node import DecisionNode, UncertaintyNode, UtilityNode + + +@pytest.fixture +def influence_diagram(): + n0 = UncertaintyNode(shortname="u1", description="Uncertainty node 1") + n1 = UncertaintyNode(shortname="u2", description="Uncertainty node 2") + n2 = UncertaintyNode(shortname="u3", description="Uncertainty node 3") + n3 = UncertaintyNode(shortname="u4", description="Uncertainty node 4") + n4 = DecisionNode(shortname="d1", description="Decision node 1") + n5 = UncertaintyNode(shortname="u5", description="Uncertainty node 5") + n6 = DecisionNode(shortname="d2", description="Decision node 2") + n7 = UncertaintyNode(shortname="u6", description="Uncertainty node 6") + n8 = UncertaintyNode(shortname="u7", description="Uncertainty node 7") + n9 = UncertaintyNode(shortname="u8", description="Uncertainty node 8") + n10 = UtilityNode(shortname="v1", description="Utility node 1") + + e0 = Arc(tail=n0, head=n4, label="e0") + e1 = Arc(tail=n1, head=n4, label="e1") + e2 = Arc(tail=n2, head=n4, label="e2") + e3 = Arc(tail=n3, head=n6, label="e3") + e4 = Arc(tail=n4, head=n6, label="e4") + e5 = Arc(tail=n4, head=n5, label="e5") + e6 = Arc(tail=n6, head=n7, label="e6") + e7 = Arc(tail=n6, head=n8, label="e7") + e8 = Arc(tail=n6, head=n9, label="e8") + e9 = Arc(tail=n5, head=n10, label="e9") + + data = { + "nodes": [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10], + "arcs": [e0, e1, e2, e3, e4, e5, e6, e7, e8, e9], + } + + diagram = InfluenceDiagram() + diagram.add_nodes(data["nodes"]) + diagram.add_arcs(data["arcs"]) + return diagram + + +def test_decision_elimination_order(influence_diagram): + result = InfluenceDiagramToDecisionTree().decision_elimination_order( + influence_diagram + ) + target = [influence_diagram.nodes[4], influence_diagram.nodes[6]] + assert all(item in target for item in result) + assert all(item in result for item in target) + + +def test_calculate_partial_order(influence_diagram): + # Test is only reproducing result, not testing logic! + partial_order = InfluenceDiagramToDecisionTree().calculate_partial_order( + influence_diagram + ) + result = [n.shortname for n in partial_order] + target = ["u1", "u2", "u3", "d1", "u4", "d2", "u5", "u6", "u7", "u8"] + assert result == target + + +def test_calculate_partial_order_fail_mode(influence_diagram, caplog): + # Test is only reproducing result, not testing logic! + with pytest.raises(Exception) as exc_info: + InfluenceDiagramToDecisionTree().calculate_partial_order( + influence_diagram, mode="junk" + ) + assert [r.msg for r in caplog.records] == [ + "output mode should be [view|copy] and have been entered as junk" + ] + assert ( + str(exc_info.value) + == "output mode should be [view|copy] and have been entered as junk" + ) + + +def test_calculate_partial_order_copy_mode(influence_diagram): + # Test is only reproducing result, not testing logic! + partial_order_0 = InfluenceDiagramToDecisionTree().calculate_partial_order( + influence_diagram, mode="view" + ) + partial_order = InfluenceDiagramToDecisionTree().calculate_partial_order( + influence_diagram, mode="copy" + ) + result = [n.shortname for n in partial_order] + target = ["u1", "u2", "u3", "d1", "u4", "d2", "u5", "u6", "u7", "u8"] + assert result == target + assert partial_order_0 != partial_order + + +def test_output_branches_from_node_empty_lists(influence_diagram): + uncertainty_node = influence_diagram.nodes[0] + decision_node = influence_diagram.nodes[4] + utility_node = influence_diagram.nodes[10] + + assert ( + len( + list( + zip( + *InfluenceDiagramToDecisionTree()._output_branches_from_node( + uncertainty_node, uncertainty_node + ), + strict=False, + ) + ) + ) + == 0 + ) + assert ( + len( + list( + zip( + *InfluenceDiagramToDecisionTree()._output_branches_from_node( + decision_node, uncertainty_node + ), + strict=False, + ) + ) + ) + == 0 + ) + assert ( + len( + list( + zip( + *InfluenceDiagramToDecisionTree()._output_branches_from_node( + utility_node, uncertainty_node + ), + strict=False, + ) + ) + ) + == 0 + ) + + +def test_output_branches_from_node(influence_diagram): + uncertainty_node = influence_diagram.nodes[0] + uncertainty_node._probability = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.7], [0.2], [0.1]]), + "variables": {"States": ["pear", "lemon", "plum"]}, + } + ) + decision_node = influence_diagram.nodes[4] + decision_node._alternatives = ["wait", "pickup"] + utility_node = influence_diagram.nodes[10] + utility_node._utility = ["1000"] + + result = list( + InfluenceDiagramToDecisionTree()._output_branches_from_node( + uncertainty_node, uncertainty_node + ) + ) + assert all(r[1] == uncertainty_node for r in result) + assert [r[0].label for r in result] == ["plum", "lemon", "pear"] + assert all(isinstance(r[0], Arc) for r in result) + assert all(r[0].tail == uncertainty_node for r in result) + assert all(r[0].head is None for r in result) + + result = list( + InfluenceDiagramToDecisionTree()._output_branches_from_node( + decision_node, uncertainty_node + ) + ) + assert all(r[1] == uncertainty_node for r in result) + assert [r[0].label for r in result] == ["pickup", "wait"] + assert all(isinstance(r[0], Arc) for r in result) + assert all(r[0].tail == decision_node for r in result) + assert all(r[0].head is None for r in result) + + result = list( + InfluenceDiagramToDecisionTree()._output_branches_from_node( + utility_node, uncertainty_node + ) + ) + assert all(r[1] == uncertainty_node for r in result) + assert [r[0].label for r in result] == ["1000"] + assert all(isinstance(r[0], Arc) for r in result) + assert all(r[0].tail == utility_node for r in result) + assert all(r[0].head is None for r in result) + + +def test_output_branches_from_node_reverse_mode(influence_diagram): + uncertainty_node = influence_diagram.nodes[0] + uncertainty_node._probability = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.7], [0.2], [0.1]]), + "variables": {"States": ["pear", "lemon", "plum"]}, + } + ) + + result = list( + InfluenceDiagramToDecisionTree()._output_branches_from_node( + uncertainty_node, uncertainty_node, flip=False + ) + ) + assert [r[0].label for r in result] == ["pear", "lemon", "plum"] + + +def test_convert_to_decision_tree_symmetry(): + # Medical Diagnosis Problem + # Data taken from + # DECISION TREES AND INFLUENCE DIAGRAMS + # Prakash P. Shenoy + # Encyclopedia of Life Support Systems, U Derigs (ed.), + # Optimization and Operations Research, Vol. 4, pp. 280–298, 2009 + # + # + # Probabilities in the ID to be updated!!! + # + # The produced tree may be flipped horizontally compared to the expected one + + probability_S = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.7], [0.3]]), + "variables": {"State": ["yes", "no"]}, + } + ) + probability_P = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.2], [0.8]]), + "variables": {"State": ["yes", "no"]}, + } + ) + probability_D = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.1], [0.9]]), + "variables": {"State": ["yes", "no"]}, + } + ) + + id_decision_T = DecisionNode( + shortname="T", description="Treat for Disease", alternatives=["yes", "no"] + ) + id_uncertainty_S = UncertaintyNode( + shortname="S", description="Symptom", probability=probability_S + ) + id_uncertainty_P = UncertaintyNode( + shortname="P", description="Pathological state", probability=probability_P + ) + id_uncertainty_D = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + id_utility_0 = UtilityNode(shortname="v", description="Utility") + + id_e0 = Arc(tail=id_uncertainty_S, head=id_decision_T) + id_e1 = Arc(tail=id_uncertainty_P, head=id_uncertainty_S) + id_e2 = Arc(tail=id_uncertainty_D, head=id_uncertainty_P) + id_e3 = Arc(tail=id_decision_T, head=id_utility_0) + id_e4 = Arc(tail=id_uncertainty_P, head=id_utility_0) + id_e5 = Arc(tail=id_uncertainty_D, head=id_utility_0) + + net = { + "nodes": [ + id_decision_T, + id_uncertainty_S, + id_uncertainty_P, + id_uncertainty_D, + id_utility_0, + ], + "arcs": [id_e0, id_e1, id_e2, id_e3, id_e4, id_e5], + } + + ID = InfluenceDiagram() + ID.add_nodes(net["nodes"]) + ID.add_arcs(net["arcs"]) + + dt_uncertainty_S = UncertaintyNode( + shortname="S", description="Symptom", probability=probability_S + ) + dt_decision_T_0 = DecisionNode( + shortname="T", description="Treat for Disease", alternatives=["yes", "no"] + ) + dt_e0 = Arc(tail=dt_uncertainty_S, head=dt_decision_T_0, label="yes") + dt_uncertainty_P_0 = UncertaintyNode( + shortname="P", description="Pathological state", probability=probability_P + ) + dt_e1 = Arc(tail=dt_decision_T_0, head=dt_uncertainty_P_0, label="yes") + dt_uncertainty_D_0 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e2 = Arc(tail=dt_uncertainty_P_0, head=dt_uncertainty_D_0, label="yes") + dt_utility_0 = UtilityNode(shortname="v", description="Utility") + dt_e3 = Arc(tail=dt_uncertainty_D_0, head=dt_utility_0, label="yes") + dt_utility_1 = UtilityNode(shortname="v", description="Utility") + dt_e4 = Arc(tail=dt_uncertainty_D_0, head=dt_utility_1, label="no") + dt_uncertainty_D_1 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e5 = Arc(tail=dt_uncertainty_P_0, head=dt_uncertainty_D_1, label="no") + dt_utility_2 = UtilityNode(shortname="v", description="Utility") + dt_e6 = Arc(tail=dt_uncertainty_D_1, head=dt_utility_2, label="yes") + dt_utility_3 = UtilityNode(shortname="v", description="Utility") + dt_e7 = Arc(tail=dt_uncertainty_D_1, head=dt_utility_3, label="no") + dt_uncertainty_P_1 = UncertaintyNode( + shortname="P", description="Pathological state", probability=probability_P + ) + dt_e8 = Arc(tail=dt_decision_T_0, head=dt_uncertainty_P_1, label="no") + dt_uncertainty_D_2 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e9 = Arc(tail=dt_uncertainty_P_1, head=dt_uncertainty_D_2, label="yes") + dt_utility_4 = UtilityNode(shortname="v", description="Utility") + dt_e10 = Arc(tail=dt_uncertainty_D_2, head=dt_utility_4, label="yes") + dt_utility_5 = UtilityNode(shortname="v", description="Utility") + dt_e11 = Arc(tail=dt_uncertainty_D_2, head=dt_utility_5, label="no") + dt_uncertainty_D_3 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e12 = Arc(tail=dt_uncertainty_P_1, head=dt_uncertainty_D_3, label="no") + dt_utility_6 = UtilityNode(shortname="v", description="Utility") + dt_e13 = Arc(tail=dt_uncertainty_D_3, head=dt_utility_6, label="yes") + dt_utility_7 = UtilityNode(shortname="v", description="Utility") + dt_e14 = Arc(tail=dt_uncertainty_D_3, head=dt_utility_7, label="no") + dt_decision_T_1 = DecisionNode( + shortname="T", description="Treat for Disease", alternatives=["yes", "no"] + ) + dt_e15 = Arc(tail=dt_uncertainty_S, head=dt_decision_T_1, label="no") + dt_uncertainty_P_2 = UncertaintyNode( + shortname="P", description="Pathological state", probability=probability_P + ) + dt_e16 = Arc(tail=dt_decision_T_1, head=dt_uncertainty_P_2, label="yes") + dt_uncertainty_D_4 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e17 = Arc(tail=dt_uncertainty_P_2, head=dt_uncertainty_D_4, label="yes") + dt_utility_8 = UtilityNode(shortname="v", description="Utility") + dt_e18 = Arc(tail=dt_uncertainty_D_4, head=dt_utility_8, label="yes") + dt_utility_9 = UtilityNode(shortname="v", description="Utility") + dt_e19 = Arc(tail=dt_uncertainty_D_4, head=dt_utility_9, label="no") + dt_uncertainty_D_5 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e20 = Arc(tail=dt_uncertainty_P_2, head=dt_uncertainty_D_5, label="no") + dt_utility_10 = UtilityNode(shortname="v", description="Utility") + dt_e21 = Arc(tail=dt_uncertainty_D_5, head=dt_utility_10, label="yes") + dt_utility_11 = UtilityNode(shortname="v", description="Utility") + dt_e22 = Arc(tail=dt_uncertainty_D_5, head=dt_utility_11, label="no") + dt_uncertainty_P_3 = UncertaintyNode( + shortname="P", description="Pathological state", probability=probability_P + ) + dt_e23 = Arc(tail=dt_decision_T_1, head=dt_uncertainty_P_3, label="no") + dt_uncertainty_D_6 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e24 = Arc(tail=dt_uncertainty_P_3, head=dt_uncertainty_D_6, label="yes") + dt_utility_12 = UtilityNode(shortname="v", description="Utility") + dt_e25 = Arc(tail=dt_uncertainty_D_6, head=dt_utility_12, label="yes") + dt_utility_13 = UtilityNode(shortname="v", description="Utility") + dt_e26 = Arc(tail=dt_uncertainty_D_6, head=dt_utility_13, label="no") + dt_uncertainty_D_7 = UncertaintyNode( + shortname="D", description="Disease", probability=probability_D + ) + dt_e27 = Arc(tail=dt_uncertainty_P_3, head=dt_uncertainty_D_7, label="no") + dt_utility_14 = UtilityNode(shortname="v", description="Utility") + dt_e28 = Arc(tail=dt_uncertainty_D_7, head=dt_utility_14, label="yes") + dt_utility_15 = UtilityNode(shortname="v", description="Utility") + dt_e29 = Arc(tail=dt_uncertainty_D_7, head=dt_utility_15, label="no") + + nodes = [ + dt_uncertainty_S, + dt_decision_T_0, + dt_uncertainty_P_0, + dt_uncertainty_D_0, + dt_utility_0, + dt_utility_1, + dt_uncertainty_D_1, + dt_utility_2, + dt_utility_3, + dt_uncertainty_P_1, + dt_uncertainty_D_2, + dt_utility_4, + dt_utility_5, + dt_uncertainty_D_3, + dt_utility_6, + dt_utility_7, + dt_decision_T_1, + dt_uncertainty_P_2, + dt_uncertainty_D_4, + dt_utility_8, + dt_utility_9, + dt_uncertainty_D_5, + dt_utility_10, + dt_utility_11, + dt_uncertainty_P_3, + dt_uncertainty_D_6, + dt_utility_12, + dt_utility_13, + dt_uncertainty_D_7, + dt_utility_14, + dt_utility_15, + ] + net = { + "nodes": nodes, + "arcs": [ + dt_e0, + dt_e1, + dt_e2, + dt_e3, + dt_e4, + dt_e5, + dt_e6, + dt_e7, + dt_e8, + dt_e9, + dt_e10, + dt_e11, + dt_e12, + dt_e13, + dt_e14, + dt_e15, + dt_e16, + dt_e17, + dt_e18, + dt_e19, + dt_e20, + dt_e21, + dt_e22, + dt_e23, + dt_e24, + dt_e25, + dt_e26, + dt_e27, + dt_e28, + dt_e29, + ], + } + + DT = DecisionTree(root=dt_uncertainty_S) + DT.add_nodes(net["nodes"]) + DT.add_arcs(net["arcs"]) + + IDT = InfluenceDiagramToDecisionTree().conversion(ID) + for id_node, dt_node in zip(IDT.graph.nodes, DT.graph.nodes, strict=False): + assert type(id_node) is type(dt_node) + assert id_node.description == dt_node.description + if not isinstance(id_node, UtilityNode): + assert id_node.shortname == dt_node.shortname + + for id_edge, dt_edge in zip(IDT.graph.edges, DT.graph.edges, strict=False): + assert type(id_edge) is type(dt_edge) + assert id_edge[0].description == dt_edge[0].description + assert id_edge[1].description == dt_edge[1].description + assert ( + IDT.graph.edges[id_edge[0], id_edge[1]]["label"] + == DT.graph.edges[dt_edge[0], dt_edge[1]]["label"] + ) + assert ( + IDT.graph.edges[id_edge[0], id_edge[1]]["dtype"] + == DT.graph.edges[dt_edge[0], dt_edge[1]]["dtype"] + ) + + +def test_convert_to_decision_tree_simple_order_asymmetry(): + probability_U1 = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.7], [0.3]]), + "variables": {"State": ["high", "low"]}, + } + ) + probability_U2 = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.2], [0.8]]), + "variables": {"State": ["yes", "no"]}, + } + ) + + id_uncertainty_u1 = UncertaintyNode( + shortname="u1", description="U1", probability=probability_U1 + ) + id_uncertainty_u2 = UncertaintyNode( + shortname="u2", description="U2", probability=probability_U2 + ) + id_decision = DecisionNode( + shortname="D", description="D", alternatives=["green", "red"] + ) + + id_e0 = Arc(tail=id_uncertainty_u1, head=id_decision) + id_e1 = Arc(tail=id_uncertainty_u2, head=id_decision) + + net = { + "nodes": [id_uncertainty_u1, id_uncertainty_u2, id_decision], + "arcs": [id_e0, id_e1], + } + + ID = InfluenceDiagram() + ID.add_nodes(net["nodes"]) + ID.add_arcs(net["arcs"]) + + dt_uncertainty_u1 = UncertaintyNode( + shortname="u1", description="U1", probability=probability_U1 + ) + + dt_uncertainty_u2_1 = UncertaintyNode( + shortname="u2", description="U2", probability=probability_U2 + ) + dt_e0 = Arc(tail=dt_uncertainty_u1, head=dt_uncertainty_u2_1, label="high") + dt_decision_1 = DecisionNode( + shortname="D", description="D", alternatives=["green", "red"] + ) + dt_e1 = Arc(tail=dt_uncertainty_u2_1, head=dt_decision_1, label="yes") + dt_utility_0 = UtilityNode(shortname="v", description="Utility") + dt_e2 = Arc(tail=dt_decision_1, head=dt_utility_0, label="green") + dt_utility_1 = UtilityNode(shortname="v", description="Utility") + dt_e3 = Arc(tail=dt_decision_1, head=dt_utility_1, label="red") + + dt_decision_2 = DecisionNode( + shortname="D", description="D", alternatives=["green", "red"] + ) + dt_e4 = Arc(tail=dt_uncertainty_u2_1, head=dt_decision_2, label="no") + dt_utility_2 = UtilityNode(shortname="v", description="Utility") + dt_e5 = Arc(tail=dt_decision_2, head=dt_utility_2, label="green") + dt_utility_3 = UtilityNode(shortname="v", description="Utility") + dt_e6 = Arc(tail=dt_decision_2, head=dt_utility_3, label="red") + + dt_uncertainty_u2_2 = UncertaintyNode( + shortname="u2", description="U2", probability=probability_U2 + ) + dt_e7 = Arc(tail=dt_uncertainty_u1, head=dt_uncertainty_u2_2, label="low") + dt_decision_3 = DecisionNode( + shortname="D", description="D", alternatives=["green", "red"] + ) + dt_e8 = Arc(tail=dt_uncertainty_u2_2, head=dt_decision_3, label="yes") + dt_utility_4 = UtilityNode(shortname="v", description="Utility") + dt_e9 = Arc(tail=dt_decision_3, head=dt_utility_4, label="green") + dt_utility_5 = UtilityNode(shortname="v", description="Utility") + dt_e10 = Arc(tail=dt_decision_3, head=dt_utility_5, label="red") + + dt_decision_4 = DecisionNode( + shortname="D", description="D", alternatives=["green", "red"] + ) + dt_e11 = Arc(tail=dt_uncertainty_u2_2, head=dt_decision_4, label="no") + dt_utility_6 = UtilityNode(shortname="v", description="Utility") + dt_e12 = Arc(tail=dt_decision_4, head=dt_utility_6, label="green") + dt_utility_7 = UtilityNode(shortname="v", description="Utility") + dt_e13 = Arc(tail=dt_decision_4, head=dt_utility_7, label="red") + + nodes = [ + dt_uncertainty_u1, + dt_uncertainty_u2_1, + dt_decision_1, + dt_utility_0, + dt_utility_1, + dt_decision_2, + dt_utility_2, + dt_utility_3, + dt_uncertainty_u2_2, + dt_decision_3, + dt_utility_4, + dt_utility_5, + dt_decision_4, + dt_utility_6, + dt_utility_7, + ] + net = { + "nodes": nodes, + "arcs": [ + dt_e0, + dt_e1, + dt_e2, + dt_e3, + dt_e4, + dt_e5, + dt_e6, + dt_e7, + dt_e8, + dt_e9, + dt_e10, + dt_e11, + dt_e12, + dt_e13, + ], + } + + DT = DecisionTree(root=dt_uncertainty_u1) + DT.add_nodes(net["nodes"]) + DT.add_arcs(net["arcs"]) + + IDT = InfluenceDiagramToDecisionTree().conversion(ID) + for id_node, dt_node in zip(IDT.graph.nodes, DT.graph.nodes, strict=False): + assert type(id_node) is type(dt_node) + assert id_node.description == dt_node.description + if not isinstance(id_node, UtilityNode): + assert id_node.shortname == dt_node.shortname + + for id_edge, dt_edge in zip(IDT.graph.edges, DT.graph.edges, strict=False): + assert type(id_edge) is type(dt_edge) + assert id_edge[0].description == dt_edge[0].description + assert id_edge[1].description == dt_edge[1].description + assert ( + IDT.graph.edges[id_edge[0], id_edge[1]]["label"] + == DT.graph.edges[dt_edge[0], dt_edge[1]]["label"] + ) + assert ( + IDT.graph.edges[id_edge[0], id_edge[1]]["dtype"] + == DT.graph.edges[dt_edge[0], dt_edge[1]]["dtype"] + ) + + +# def test_convert_to_decision_tree_asymemetry(): +# TEST NEEDS TO BE UPDATED !!! + +# # Wildcatter example +# # Data taken from +# # An improved method for solving Hybrid Influence Diagrams +# # Barbaros Yet, Martin Neil, Norman Fenton, Anthony Constantinou, +# # Eugene Dementiev +# # International Journal of Approximate Reasoning, Volume 95, April 2018, +# # Pages 93-112 +# # https://doi.org/10.1016/j.ijar.2018.01.006 +# # +# # +# # Probabilities in the ID to be updated!!! +# # +# # The produced tree may be flipped horizontally compared to the expected one +# id_decision_T = DecisionNode("Seismic Test", "T", alternatives=["yes", "No"]) +# id_decision_D = DecisionNode("Drill", "D", alternatives=["yes", "No"]) +# id_uncertainty_R = UncertaintyNode("Test Result", "R", \ +# probabilities={'No': 0.410, 'Open': 0.350, 'Closed': 0.240}) +# id_uncertainty_O = UncertaintyNode("Oil", "O", \ +# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) +# id_value_S = UtilityNode("Seismic Cost", "U1", utility=0) +# id_value_D = UtilityNode("Drilling Gain", "U2", utility=0) +# id_value_T = UtilityNode("Total", "U3", utility=0) + +# id_edge_0 = Edge(id_decision_T, id_value_S) +# id_edge_1 = Edge(id_decision_T, id_uncertainty_R) +# id_edge_2 = Edge(id_decision_T, id_decision_D) +# id_edge_3 = Edge(id_uncertainty_R, id_decision_D) +# id_edge_4 = Edge(id_uncertainty_O, id_uncertainty_R) +# id_edge_5 = Edge(id_uncertainty_O, id_value_D) +# id_edge_6 = Edge(id_decision_D, id_value_D) +# id_edge_7 = Edge(id_value_S, id_value_T) +# id_edge_8 = Edge(id_value_D, id_value_T) + +# net = {'nodes': [id_decision_T, id_decision_D, id_uncertainty_R, \ +# id_uncertainty_O, id_value_S, id_value_D, id_value_T], +# 'edges': [id_edge_0, id_edge_1, id_edge_2, id_edge_3, id_edge_4, \ +# id_edge_5, id_edge_6, id_edge_7, id_edge_8]} + +# ID = InfluenceDiagram() +# for node in net['nodes']: +# ID.add_node(node) +# for edge in net['edges']: +# ID.add_edge(edge) + + +# dt_decision_T_0 = DecisionNode("Seismic Test", "T", alternatives=["yes", "No"]) +# dt_decision_R_0 = UncertaintyNode("Test Result", "R", \ +# probabilities={'No': 0.410, 'Open': 0.350, 'Closed': 0.240}) +# dt_edge_0 = Edge(dt_decision_T_0, dt_decision_R_0, name="Yes") +# dt_decision_D_0 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) +# dt_edge_1 = Edge(dt_decision_R_0, dt_decision_D_0, name="No") +# dt_uncertainty_O_0 = UncertaintyNode("Oil", "O", \ +# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) +# dt_edge_2 = Edge(dt_decision_D_0, dt_uncertainty_O_0, name="Yes") +# dt_value_T_0 = UtilityNode("Total", "U3", utility=0) +# dt_edge_3 = Edge(dt_uncertainty_O_0, dt_value_T_0, name="Dry") +# dt_value_T_1 = UtilityNode("Total", "U3", utility=0) +# dt_edge_4 = Edge(dt_uncertainty_O_0, dt_value_T_1, name="Wet") +# dt_value_T_2 = UtilityNode("Total", "U3", utility=0) +# dt_edge_5 = Edge(dt_uncertainty_O_0, dt_value_T_2, name="Soaking") +# dt_value_T_3 = UtilityNode("Total", "U3", utility=0) +# dt_edge_6 = Edge(dt_decision_D_0, dt_value_T_3, name="No") +# dt_decision_D_1 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) +# dt_edge_7 = Edge(dt_decision_R_0, dt_decision_D_1, name="Open") +# dt_uncertainty_O_1 = UncertaintyNode("Oil", "O", \ +# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) +# dt_edge_8 = Edge(dt_decision_D_1, dt_uncertainty_O_1, name="Yes") +# dt_value_T_4 = UtilityNode("Total", "U3", utility=0) +# dt_edge_9 = Edge(dt_uncertainty_O_1, dt_value_T_4, name="Dry") +# dt_value_T_5 = UtilityNode("Total", "U3", utility=0) +# dt_edge_10 = Edge(dt_uncertainty_O_1, dt_value_T_5, name="Wet") +# dt_value_T_6 = UtilityNode("Total", "U3", utility=0) +# dt_edge_11 = Edge(dt_uncertainty_O_1, dt_value_T_6, name="Soaking") +# dt_value_T_7 = UtilityNode("Total", "U3", utility=0) +# dt_edge_12 = Edge(dt_decision_D_1, dt_value_T_7, name="No") +# dt_decision_D_2 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) +# dt_edge_13 = Edge(dt_decision_R_0, dt_decision_D_2, name="Closed") +# dt_uncertainty_O_2 = UncertaintyNode("Oil", "O", \ +# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) +# dt_edge_14 = Edge(dt_decision_D_2, dt_uncertainty_O_2, name="Yes") +# dt_value_T_8 = UtilityNode("Total", "U3", utility=0) +# dt_edge_15 = Edge(dt_uncertainty_O_2, dt_value_T_8, name="Dry") +# dt_value_T_9 = UtilityNode("Total", "U3", utility=0) +# dt_edge_16 = Edge(dt_uncertainty_O_2, dt_value_T_9, name="Wet") +# dt_value_T_10 = UtilityNode("Total", "U3", utility=0) +# dt_edge_17 = Edge(dt_uncertainty_O_2, dt_value_T_10, name="Soaking") +# dt_value_T_11 = UtilityNode("Total", "U3", utility=0) +# dt_edge_18 = Edge(dt_decision_D_2, dt_value_T_11, name="No") +# dt_decision_D_3 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) +# dt_edge_19 = Edge(dt_decision_T_0, dt_decision_D_3, name="No") +# dt_uncertainty_O_3 = UncertaintyNode("Oil", "O", \ +# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) +# dt_edge_20 = Edge(dt_decision_D_2, dt_uncertainty_O_3, name="Yes") +# dt_value_T_12 = UtilityNode("Total", "U3", utility=0) +# dt_edge_21 = Edge(dt_uncertainty_O_3, dt_value_T_12, name="Dry") +# dt_value_T_13 = UtilityNode("Total", "U3", utility=0) +# dt_edge_22 = Edge(dt_uncertainty_O_3, dt_value_T_13, name="Wet") +# dt_value_T_14 = UtilityNode("Total", "U3", utility=0) +# dt_edge_23 = Edge(dt_uncertainty_O_3, dt_value_T_14, name="Soaking") +# dt_value_T_15 = UtilityNode("Total", "U3", utility=0) +# dt_edge_24 = Edge(dt_decision_D_3, dt_value_T_15, name="No") + + +# net = {'nodes': [dt_decision_T_0, +# dt_decision_R_0, +# dt_decision_D_0, +# dt_uncertainty_O_0, +# dt_value_T_0, +# dt_value_T_1, +# dt_value_T_2, +# dt_value_T_3, +# dt_decision_D_1, +# dt_uncertainty_O_1, +# dt_value_T_4, +# dt_value_T_5, +# dt_value_T_6, +# dt_value_T_7, +# dt_decision_D_2, +# dt_uncertainty_O_2, +# dt_value_T_8, +# dt_value_T_9, +# dt_value_T_10, +# dt_value_T_11, +# dt_decision_D_3, +# dt_uncertainty_O_3, +# dt_value_T_12, +# dt_value_T_13, +# dt_value_T_14, +# dt_value_T_15], +# 'edges': [dt_edge_0, +# dt_edge_1, +# dt_edge_2, +# dt_edge_3, +# dt_edge_4, +# dt_edge_5, +# dt_edge_6, +# dt_edge_7, +# dt_edge_8, +# dt_edge_8, +# dt_edge_9, +# dt_edge_10, +# dt_edge_11, +# dt_edge_12, +# dt_edge_13, +# dt_edge_14, +# dt_edge_15, +# dt_edge_16, +# dt_edge_17, +# dt_edge_18, +# dt_edge_19, +# dt_edge_20, +# dt_edge_21, +# dt_edge_22, +# dt_edge_23, +# dt_edge_24]} + +# DT = DecisionTree() +# for node in net['nodes']: +# DT.add_node(node) +# for edge in net['edges']: +# DT.add_edge(edge) + +# assert ID.convert_to_decision_tree() == DT diff --git a/api/tests/v0/services/analysis/test_id_to_pyagrum.py b/api/tests/v0/services/analysis/test_id_to_pyagrum.py new file mode 100644 index 0000000..2abf082 --- /dev/null +++ b/api/tests/v0/services/analysis/test_id_to_pyagrum.py @@ -0,0 +1,123 @@ +import json + +import numpy as np +import pyAgrum as gum +import pytest + +from src.v0.services.analysis.id_to_pyagrum import InfluenceDiagramToPyAgrum +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) +from src.v0.services.format_conversions.directed_graph import InfluenceDiagramConversion + +TESTDATA = "v0/services/testdata" + + +@pytest.fixture +def influence_diagram(copy_testdata_tmpdir, tmp_path): + copy_testdata_tmpdir(TESTDATA) + with open(tmp_path / "used_car_buyer_problem.json") as f: + data = json.load(f) + issues = data["vertices"]["issues"] + issues = [ + {"uuid" if k == "id" else k: v for k, v in issue.items()} for issue in issues + ] + data = { + "vertices": issues, + "edges": [edge for edge in data["edges"] if edge["label"] == "influences"], + } + diagram = InfluenceDiagramConversion().from_json(data) + return diagram + + +def test_conversion_used_car_buyer_success(influence_diagram): + result = InfluenceDiagramToPyAgrum().conversion(influence_diagram) + assert isinstance(result, gum.InfluenceDiagram) + assert result.size() == 5 + assert result.chanceNodeSize() == 2 + assert result.decisionNodeSize() == 2 + assert result.utilityNodeSize() == 1 + + # import pyAgrum.lib.image as gimg + # from shutil import copyfile + # gimg.export(result, "test2.png") + # result.saveBIFXML("test.bifxml") + # testroot = '/tmp/pytest-of-codespace/pytest-current/' + # testpath = testroot+'test_conversion_used_car_buyercurrent/' + # copyfile(testpath+"test2.png", "/workspaces/dot/test2.png") + # copyfile(testpath+"test.bifxml", "/workspaces/dot/test.bifxml") + + +def test_to_pyagrum_used_car_buyer_not_acyclic_fail(influence_diagram, caplog): + node0 = list(influence_diagram.graph.nodes)[0] + node1 = list(influence_diagram.graph.nodes)[1] + edge1 = Arc(tail=node0, head=node1, label="cyclic") + edge2 = Arc(tail=node1, head=node0, label="cyclic") + influence_diagram.add_arc(edge1) + influence_diagram.add_arc(edge2) + + with pytest.raises(Exception) as exc_info: + InfluenceDiagramToPyAgrum().conversion(influence_diagram) + assert [r.msg for r in caplog.records] == [ + "the influence diagram is not acyclic: False" + ] + assert str(exc_info.value) == "the influence diagram is not acyclic: False" + + +def test_to_pyagrum_used_car_buyer_uncertainty_2d_unconditional_fail( + influence_diagram, caplog +): + for node in influence_diagram.nodes: + node.probability = DiscreteUnconditionalProbability( + **{ + "probability_function": np.array([[0.1, 0.2], [0.3, 0.4]]), + "variables": {"v1": ["blue", "green"], "v2": ["high", "low"]}, + } + ) + with pytest.raises(Exception) as exc_info: + InfluenceDiagramToPyAgrum().conversion(influence_diagram) + assert ( + "Input probability cannot be used in pyagrum with error:" + in [r.msg for r in caplog.records][0] + ) + assert "Input probability cannot be used in pyagrum with error:" in str( + exc_info.value + ) + + +def test_to_pyagrum_used_car_buyer_uncertainty_fail(influence_diagram, caplog): + for node in influence_diagram.nodes: + node.probability = None + with pytest.raises(Exception) as exc_info: + InfluenceDiagramToPyAgrum().conversion(influence_diagram) + assert [r.msg for r in caplog.records] == [ + ( + "Input probability cannot be used in pyagrum with error: [pyAgrum] " + "Invalid argument: Empty variable State:Labelized({}) " + "cannot be added in a Potential" + ) + ] + assert str(exc_info.value) == ( + "Input probability cannot be used in pyagrum with error: [pyAgrum] " + "Invalid argument: Empty variable State:Labelized({}) " + "cannot be added in a Potential" + ) + + +def test_to_pyagrum_used_car_buyer_decision_fail(influence_diagram, caplog): + for node in influence_diagram.nodes: + node.alternatives = None + + with pytest.raises(Exception) as exc_info: + InfluenceDiagramToPyAgrum().conversion(influence_diagram) + assert [r.msg for r in caplog.records] == [ + "Input arc cannot be used in pyagrum with error: " + "[pyAgrum] Invalid argument: Empty variable Test:Labelized({}) " + "cannot be added in a Potential" + ] + assert str(exc_info.value) == ( + "Input arc cannot be used in pyagrum with error: " + "[pyAgrum] Invalid argument: Empty variable Test:Labelized({}) " + "cannot be added in a Potential" + ) diff --git a/api/tests/v0/services/structure_utils/test_decision_diagrams/__init__,py b/api/tests/v0/services/class_validations/__init__.py similarity index 100% rename from api/tests/v0/services/structure_utils/test_decision_diagrams/__init__,py rename to api/tests/v0/services/class_validations/__init__.py diff --git a/api/tests/v0/services/class_validations/test_validate_and_set_arc.py b/api/tests/v0/services/class_validations/test_validate_and_set_arc.py new file mode 100644 index 0000000..714767f --- /dev/null +++ b/api/tests/v0/services/class_validations/test_validate_and_set_arc.py @@ -0,0 +1,92 @@ +from uuid import UUID, uuid1, uuid4 + +import pytest + +from src.v0.services.class_validations import validate_and_set_arc +from src.v0.services.classes.node import DecisionNode + + +def test_label_success_string(): + assert validate_and_set_arc.label("None") == "None" + + +def test_label_success_None(): + assert validate_and_set_arc.label(None) is None + + +def test_label_fail(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_arc.label(1.0) + assert [r.msg for r in caplog.records] == [ + "Input label is neither a string nor None: 1.0" + ] + assert str(exc_info.value) == "Input label is neither a string nor None: 1.0" + + +def test_edge_success_None(): + assert validate_and_set_arc.edge(None) is None + + +def test_edge_success_Node(): + node = DecisionNode(description="decision", shortname="D") + assert isinstance(validate_and_set_arc.edge(node), DecisionNode) + + +def test_edge_fail_neither_Node_nor_None(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_arc.edge(1.0) + assert [r.msg for r in caplog.records] == [ + "Endpoint of arcs should be Node or None: 1.0" + ] + assert str(exc_info.value) == "Endpoint of arcs should be Node or None: 1.0" + + +def test_uuid_success_uuid(): + id = uuid4() + assert isinstance(validate_and_set_arc.uuid(id), str) + assert UUID(validate_and_set_arc.uuid(id)).version == 4 + assert isinstance(validate_and_set_arc.uuid(str(id)), str) + assert UUID(validate_and_set_arc.uuid(str(id))).version == 4 + + +def test_uuid_success_None(): + assert UUID(validate_and_set_arc.uuid(None)).version == 4 + + +def test_uuid_fail_not_None(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_arc.uuid(3.14) + assert [r.msg for r in caplog.records] == [ + "Input uuid is neither a valid uuid (version 4) nor None: 3.14" + ] + assert ( + str(exc_info.value) + == "Input uuid is neither a valid uuid (version 4) nor None: 3.14" + ) + + +def test_uuid_fail_not_uuid(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_arc.uuid("3.14") + assert [r.msg for r in caplog.records] == [ + ( + "Input uuid is neither a valid uuid (version 4) nor None: " + "badly formed hexadecimal UUID string" + ) + ] + assert str(exc_info.value) == ( + "Input uuid is neither a valid uuid (version 4) nor None: " + "badly formed hexadecimal UUID string" + ) + + +def test_uuid_fail_not_version_4(caplog): + id = str(uuid1()) + with pytest.raises(Exception) as exc_info: + validate_and_set_arc.uuid(id) + assert [r.msg for r in caplog.records] == [ + ("Input uuid is neither a valid " "uuid (version 4) nor None: version 1") + ] + assert str(exc_info.value) == ( + "Input uuid is neither a valid " "uuid (version 4) nor None: version 1" + ) diff --git a/api/tests/v0/services/class_validations/test_validate_and_set_directed_graph.py b/api/tests/v0/services/class_validations/test_validate_and_set_directed_graph.py new file mode 100644 index 0000000..83b5da7 --- /dev/null +++ b/api/tests/v0/services/class_validations/test_validate_and_set_directed_graph.py @@ -0,0 +1,80 @@ +import pytest + +from src.v0.services.class_validations import validate_and_set_graph_model +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.node import ( + DecisionNode, + UncertaintyNode, + UtilityNode, +) + + +def test_id_node_success(): + assert validate_and_set_graph_model.id_node( + DecisionNode(description="description", shortname="D") + ) + assert validate_and_set_graph_model.id_node( + UncertaintyNode(description="description", shortname="C") + ) + assert validate_and_set_graph_model.id_node( + UtilityNode(description="description", shortname="V") + ) + + +def test_id_fail(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_graph_model.id_node(None) + assert [r.msg for r in caplog.records] == [ + ( + "Added node is not of instance " + "(DecisionNode, UncertaintyNode, UtilityNode): None" + ) + ] + assert str(exc_info.value) == ( + "Added node is not of instance " + "(DecisionNode, UncertaintyNode, UtilityNode): None" + ) + + +def test_dt_node_success(): + assert validate_and_set_graph_model.dt_node( + DecisionNode(description="description", shortname="D") + ) + assert validate_and_set_graph_model.dt_node( + UncertaintyNode(description="description", shortname="C") + ) + assert validate_and_set_graph_model.dt_node( + UtilityNode(description="description", shortname="V") + ) + + +def test_dt_fail(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_graph_model.dt_node(None) + assert [r.msg for r in caplog.records] == [ + ( + "Added node is not of instance " + "(DecisionNode, UncertaintyNode, UtilityNode): None" + ) + ] + assert str(exc_info.value) == ( + "Added node is not of instance " + "(DecisionNode, UncertaintyNode, UtilityNode): None" + ) + + +def test_arc_to_graph_success(): + n1 = UncertaintyNode(description="description", shortname="n1") + n2 = DecisionNode(description="description", shortname="n2") + arc = Arc(tail=n1, head=n2, label="arc_label") + assert validate_and_set_graph_model.arc_to_graph(arc) == ( + (n1, n2), + {"dtype": "informational", "label": "arc_label", "uuid": arc.uuid}, + ) + + +def test_arc_to_graph_fail(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_graph_model.arc_to_graph(None) + assert [r.msg for r in caplog.records] == ["Added arc is not of instance Arc: None"] + assert str(exc_info.value) == "Added arc is not of instance Arc: None" diff --git a/api/tests/v0/services/class_validations/test_validate_and_set_node.py b/api/tests/v0/services/class_validations/test_validate_and_set_node.py new file mode 100644 index 0000000..78a953a --- /dev/null +++ b/api/tests/v0/services/class_validations/test_validate_and_set_node.py @@ -0,0 +1,185 @@ +from uuid import UUID, uuid1, uuid4 + +import numpy as np +import pytest + +from src.v0.services.class_validations import validate_and_set_node +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) + + +def test_description_success(): + assert validate_and_set_node.description("None") == "None" + + +def test_description_fail(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.description(None) + assert [r.msg for r in caplog.records] == ["Input description is not a string: None"] + assert str(exc_info.value) == "Input description is not a string: None" + + +def test_name_success(): + assert validate_and_set_node.name("None") == "None" + + +def test_name_fail(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.name(None) + assert [r.msg for r in caplog.records] == ["Input name is not a string: None"] + assert str(exc_info.value) == "Input name is not a string: None" + + +def test_shortname_success(): + assert validate_and_set_node.shortname("None") == "None" + + +def test_shortname_fail(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.shortname(None) + assert [r.msg for r in caplog.records] == ["Input shortname is not a string: None"] + assert str(exc_info.value) == "Input shortname is not a string: None" + + +def test_uuid_success_uuid(): + id = uuid4() + assert isinstance(validate_and_set_node.uuid(id), str) + assert UUID(validate_and_set_node.uuid(id)).version == 4 + assert isinstance(validate_and_set_node.uuid(str(id)), str) + assert UUID(validate_and_set_node.uuid(str(id))).version == 4 + + +def test_uuid_success_None(): + assert UUID(validate_and_set_node.uuid(None)).version == 4 + + +def test_uuid_fail_not_None(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.uuid(3.14) + assert [r.msg for r in caplog.records] == [ + ("Input uuid is neither a valid " "uuid (version 4) nor None: 3.14") + ] + assert str(exc_info.value) == ( + "Input uuid is neither a valid " "uuid (version 4) nor None: 3.14" + ) + + +def test_uuid_fail_not_uuid(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.uuid("3.14") + assert [r.msg for r in caplog.records] == [ + ( + "Input uuid is neither a valid " + "uuid (version 4) nor None: badly formed hexadecimal UUID string" + ) + ] + assert str(exc_info.value) == ( + "Input uuid is neither a valid " + "uuid (version 4) nor None: badly formed hexadecimal UUID string" + ) + + +def test_uuid_fail_not_version_4(caplog): + id = str(uuid1()) + with pytest.raises(Exception) as exc_info: + validate_and_set_node.uuid(id) + assert [r.msg for r in caplog.records] == [ + ("Input uuid is neither a valid " "uuid (version 4) nor None: version 1") + ] + assert str(exc_info.value) == ( + "Input uuid is neither a valid " "uuid (version 4) nor None: version 1" + ) + + +def test_alternatives_success_list_of_strings(): + assert validate_and_set_node.alternatives(["1", "2", "3"]) == ["1", "2", "3"] + + +def test_alternatives_success_tuple_of_strings(): + assert validate_and_set_node.alternatives(("1", "2", "3")) == ("1", "2", "3") + + +def test_alternatives_success_None(): + assert validate_and_set_node.alternatives(None) is None + + +def test_alternatives_fail_string(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.alternatives("3.14") + assert [r.msg for r in caplog.records] == [ + "Input alternatives is neither a list " + "or tuple of unique strings nor None: 3.14" + ] + assert ( + str(exc_info.value) == "Input alternatives is neither a " + "list or tuple of unique strings nor None: 3.14" + ) + + +def test_alternatives_fail_not_sequence(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.alternatives({"3.14"}) + assert [r.msg for r in caplog.records] == [ + ( + "Input alternatives is neither a list " + "or tuple of unique strings nor None: {'3.14'}" + ) + ] + assert str(exc_info.value) == ( + "Input alternatives is neither a list " + "or tuple of unique strings nor None: {'3.14'}" + ) + + +def test_alternatives_fail_list_of_lists(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.alternatives([["3.14"]]) + assert [r.msg for r in caplog.records] == [ + ( + "Input alternatives is neither a list " + "or tuple of unique strings nor None: [['3.14']]" + ) + ] + assert str(exc_info.value) == ( + "Input alternatives is neither a list " + "or tuple of unique strings nor None: [['3.14']]" + ) + + +def test_alternatives_fail_redundant_elements(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.alternatives(["a", "a", "b"]) + assert [r.msg for r in caplog.records] == [ + ( + "Input alternatives is neither a list or " + "tuple of unique strings nor None: ['a', 'a', 'b']" + ) + ] + assert str(exc_info.value) == ( + "Input alternatives is neither a list " + "or tuple of unique strings nor None: ['a', 'a', 'b']" + ) + + +def test_probability_success_None(): + assert validate_and_set_node.probability(None) is None + + +def test_probability_success_well_formed_probability(): + values = np.array([0.1, 0.9]) + coords = {"A": ["yes", "no"]} + probability = DiscreteUnconditionalProbability(values, coords) + assert validate_and_set_node.probability(probability) == probability + + +def test_alternatives_fail_neither_None_nor_well_formed_probability(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_node.probability([["3.14"]]) + assert [r.msg for r in caplog.records] == [ + "Input probability is neither a well formed probability nor None: [['3.14']]" + ] + assert ( + str(exc_info.value) + == "Input probability is neither a well formed probability nor None: [['3.14']]" + ) diff --git a/api/tests/v0/services/class_validations/test_validate_and_set_probability.py b/api/tests/v0/services/class_validations/test_validate_and_set_probability.py new file mode 100644 index 0000000..15c31c8 --- /dev/null +++ b/api/tests/v0/services/class_validations/test_validate_and_set_probability.py @@ -0,0 +1,305 @@ +import numpy as np +import pytest + +from src.v0.services.class_validations import validate_and_set_probability + + +def test_discrete_probability_variable_success_with_reformating(): + assert validate_and_set_probability.discrete_variables( + {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + ) == { + "v1": ["y", "n"], + "v_2": ["r", "g", "b"], + } + + +def test_discrete_probability_variable_fail_not_dict(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_variables(None) + assert [r.msg for r in caplog.records] == [ + ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: None" + ) + ] + assert str(exc_info.value) == ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: None" + ) + + +def test_discrete_probability_variable_fail_outcomes_not_as_list(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_variables({"v1": ["y", "n"], "v 2": 3}) + assert [r.msg for r in caplog.records] == [ + ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: {'v1': ['y', 'n'], 'v 2': 3}" + ) + ] + assert str(exc_info.value) == ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: {'v1': ['y', 'n'], 'v 2': 3}" + ) + + +def test_discrete_probability_variable_fail_not_as_array(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_variables( + {"v1": ["y", "n"], "v 2": [["r", "g"], "b"]} + ) + assert [r.msg for r in caplog.records] == [ + ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: " + "setting an array element with a sequence. " + "The requested array has an inhomogeneous shape " + "after 1 dimensions. The detected shape was (2,) + inhomogeneous part." + ) + ] + assert str(exc_info.value) == ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: " + "setting an array element with a sequence. " + "The requested array has an inhomogeneous shape " + "after 1 dimensions. The detected shape was (2,) + inhomogeneous part." + ) + + +def test_discrete_probability_variable_fail_not_as_1d(caplog): + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_variables( + {"v1": ["y", "n"], "v 2": [["r", "g"], ["b", "a"]]} + ) + assert [r.msg for r in caplog.records] == [ + ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: " + "{'v1': ['y', 'n'], 'v 2': [['r', 'g'], ['b', 'a']]}" + ) + ] + assert str(exc_info.value) == ( + "One of the variables is not a dictionary with " + "element being able to be interpreted as 1D: " + "{'v1': ['y', 'n'], 'v 2': [['r', 'g'], ['b', 'a']]}" + ) + + +def test_discrete_conditional_probability_function_success_as_nan(): + conditioned_variable = {"v1": ["y", "n"]} + conditioning_variable = {"v 2": ["r", "g", "b"]} + pdf = np.full((2, 3), np.nan) + result = validate_and_set_probability.discrete_conditional_probability_function( + pdf, conditioned_variable, conditioning_variable + ) + assert all(np.isnan(result).tolist()) + + +def test_discrete_conditional_probability_function_success_as_normalized(): + conditioned_variable = {"v1": ["y", "n"]} + conditioning_variable = {"v 2": ["r", "g", "b"]} + pdf = np.array([[0.5, 1, 0], [0.5, 0, 1.0]]) + result = validate_and_set_probability.discrete_conditional_probability_function( + pdf, conditioned_variable, conditioning_variable + ) + np.testing.assert_allclose(pdf, result) + + +def test_discrete_conditional_probability_function_fail_not_array_like(caplog): + conditioned_variable = {"v1": ["y", "n"]} + conditioning_variable = {"v 2": ["r", "g", "b"]} + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_conditional_probability_function( + None, conditioned_variable, conditioning_variable + ) + assert [r.msg for r in caplog.records] == [ + ( + "The conditional probability function is not well " + "formed size (not compatible with variables or " + "content is not normalized): None" + ) + ] + assert str(exc_info.value) == ( + "The conditional probability function is not well " + "formed size (not compatible with variables or " + "content is not normalized): None" + ) + + +def test_discrete_conditional_probability_function_fail_not_normalized(caplog): + conditioned_variable = {"v1": ["y", "n"]} + conditioning_variable = {"v 2": ["r", "g", "b"]} + pdf = np.array([[0.95, 1, 0], [0.5, 0, 1.0]]) + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_conditional_probability_function( + pdf, conditioned_variable, conditioning_variable + ) + assert [r.msg for r in caplog.records] == [ + ( + "The conditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[0.95 1. 0. ]\n [0.5 0. 1. ]]" + ) + ] + assert str(exc_info.value) == ( + "The conditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[0.95 1. 0. ]\n [0.5 0. 1. ]]" + ) + + +def test_discrete_conditional_probability_function_fail_not_between_zero_and_one(caplog): + conditioned_variable = {"v1": ["y", "n"]} + conditioning_variable = {"v 2": ["r", "g", "b"]} + pdf = np.array([[-1.5, 1, 0], [0.5, 0, 1.0]]) + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_conditional_probability_function( + pdf, conditioned_variable, conditioning_variable + ) + assert [r.msg for r in caplog.records] == [ + ( + "The conditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[-1.5 1. 0. ]\n [ 0.5 0. 1. ]]" + ) + ] + assert str(exc_info.value) == ( + "The conditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[-1.5 1. 0. ]\n [ 0.5 0. 1. ]]" + ) + + +def test_discrete_conditional_probability_function_fail_not_consistent(caplog): + conditioned_variable = {"v1": ["y", "n"]} + conditioning_variable = {"v 2": ["r", "g", "b"]} + pdf = np.array([[0, 1, 0], [0.5, 0, 0.5], [1, 0, 0]]) + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_conditional_probability_function( + pdf, conditioned_variable, conditioning_variable + ) + assert [r.msg for r in caplog.records] == [ + ( + "The conditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[0. 1. 0. ]\n [0.5 0. 0.5]\n [1. 0. 0. ]]" + ) + ] + assert str(exc_info.value) == ( + "The conditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[0. 1. 0. ]\n [0.5 0. 0.5]\n [1. 0. 0. ]]" + ) + + +def test_discrete_unconditional_probability_function_success_as_None(): + variables = {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + probability_function = [[None, None, None], [None, None, None]] + pdf = np.array(probability_function) + result = validate_and_set_probability.discrete_unconditional_probability_function( + pdf, variables + ) + assert all(np.isnan(result).tolist()) + + +def test_discrete_unconditional_probability_function_success_as_nan(): + variables = {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + pdf = np.full((2, 3), np.nan) + result = validate_and_set_probability.discrete_unconditional_probability_function( + pdf, variables + ) + assert all(np.isnan(result).tolist()) + + +def test_discrete_unconditional_probability_function_success_as_normalized(): + variables = {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + pdf = np.array([[0, 0.25, 0], [0.25, 0, 0.5]]) + result = validate_and_set_probability.discrete_unconditional_probability_function( + pdf, variables + ) + np.testing.assert_allclose(pdf, result) + + +def test_discrete_unconditional_probability_function_fail_not_array_like(caplog): + variables = {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_unconditional_probability_function( + None, variables + ) + assert [r.msg for r in caplog.records] == [ + ( + "The unconditional probability function is not well " + "formed size (not compatible with variables or content " + "is not normalized): None" + ) + ] + assert str(exc_info.value) == ( + "The unconditional probability function is not well " + "formed size (not compatible with variables or content " + "is not normalized): None" + ) + + +def test_discrete_unconditional_probability_function_fail_not_normalized(caplog): + variables = {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + pdf = np.array([[1, 1, 0], [0.5, 0, 0.5]]) + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_unconditional_probability_function( + pdf, variables + ) + assert [r.msg for r in caplog.records] == [ + ( + "The unconditional probability function is not well formed " + "size (not compatible with variables or content is not normalized): " + "[[1. 1. 0. ]\n [0.5 0. 0.5]]" + ) + ] + assert str(exc_info.value) == ( + "The unconditional probability function is not well formed " + "size (not compatible with variables or content is not normalized): " + "[[1. 1. 0. ]\n [0.5 0. 0.5]]" + ) + + +def test_discrete_unconditional_probability_function_fail_not_between_zero_and_one( + caplog, +): + variables = {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + pdf = np.array([[10, 1, 0], [0.5, 0, 0.5]]) + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_unconditional_probability_function( + pdf, variables + ) + assert [r.msg for r in caplog.records] == [ + ( + "The unconditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[10. 1. 0. ]\n [ 0.5 0. 0.5]]" + ) + ] + assert str(exc_info.value) == ( + "The unconditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[10. 1. 0. ]\n [ 0.5 0. 0.5]]" + ) + + +def test_discrete_unconditional_probability_function_fail_not_consistent(caplog): + variables = {"v1": ["y", "n"], "v 2": ["r", "g", "b"]} + pdf = np.array([[0, 1, 0], [0.5, 0, 0.5], [1, 0, 0]]) + with pytest.raises(Exception) as exc_info: + validate_and_set_probability.discrete_unconditional_probability_function( + pdf, variables + ) + assert [r.msg for r in caplog.records] == [ + ( + "The unconditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[0. 1. 0. ]\n [0.5 0. 0.5]\n [1. 0. 0. ]]" + ) + ] + assert str(exc_info.value) == ( + "The unconditional probability function is not well " + "formed size (not compatible with variables or content is not normalized): " + "[[0. 1. 0. ]\n [0.5 0. 0.5]\n [1. 0. 0. ]]" + ) diff --git a/api/tests/v0/services/classes/__init__.py b/api/tests/v0/services/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/tests/v0/services/classes/test_abstract_probability.py b/api/tests/v0/services/classes/test_abstract_probability.py new file mode 100644 index 0000000..cf0d372 --- /dev/null +++ b/api/tests/v0/services/classes/test_abstract_probability.py @@ -0,0 +1,24 @@ +import pytest + +from src.v0.services.classes.abstract_probability import ProbabilityABC + + +def test_class_ProbabilityABC(monkeypatch): + monkeypatch.setattr( + ProbabilityABC, + "__abstractmethods__", + set(), + ) + abstract_probability = ProbabilityABC() + + with pytest.raises(NotImplementedError): + abstract_probability.initialize_nan(None) + + with pytest.raises(NotImplementedError): + abstract_probability.initialize_uniform(None) + + with pytest.raises(NotImplementedError): + abstract_probability.variables() + + with pytest.raises(NotImplementedError): + abstract_probability.get_distribution() diff --git a/api/tests/v0/services/classes/test_arc.py b/api/tests/v0/services/classes/test_arc.py new file mode 100644 index 0000000..a178aa3 --- /dev/null +++ b/api/tests/v0/services/classes/test_arc.py @@ -0,0 +1,131 @@ +import pytest + +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.node import DecisionNode, UncertaintyNode, UtilityNode + + +def test_class_InformationalArc(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = DecisionNode(description="junky", shortname="H") + arc = Arc(tail=n1, head=n2, label="first") + assert arc.tail == n1 + assert arc.head == n2 + assert arc.label == "first" + assert arc.dtype == "informational" + + +def test_class_InformationalArc_without_name(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = DecisionNode(description="junky", shortname="H") + arc = Arc(tail=n1, head=n2) + assert arc.label is None + + +def test_class_set_name(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = DecisionNode(description="junky", shortname="H") + arc = Arc(tail=n1, head=n2) + arc.label = "C2H5OH" + assert arc.label == "C2H5OH" + + +def test_class_ConditionalArc(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = UncertaintyNode(description="junky", shortname="H") + arc = Arc(tail=n1, head=n2, label="second") + assert arc.tail.description == "junk" + assert arc.tail.shortname == "J" + assert arc.head.description == "junky" + assert arc.head.shortname == "H" + assert arc.label == "second" + assert arc.dtype == "conditional" + + +def test_class_FunctionalArc(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = UtilityNode(description="junky", shortname="H") + arc = Arc(tail=n1, head=n2, label="first") + assert arc.tail == n1 + assert arc.head == n2 + assert arc.label == "first" + assert arc.dtype == "functional" + + +def test_copy(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = DecisionNode(description="junky", shortname="H") + arc = Arc(tail=n1, head=n2, label="first") + copied_arc = arc.copy() + assert copied_arc.tail == arc.tail + assert copied_arc.head == arc.head + assert copied_arc.label == arc.label + + +def test_set_head(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = DecisionNode(description="junky", shortname="H") + arc = Arc(tail=n1, head=None, label="first") + assert arc.tail == n1 + assert arc.head is None + assert arc.label == "first" + assert arc.dtype is None + arc.head = n2 + assert arc.head == n2 + assert arc.dtype == "informational" + + +def test_set_head_fail(caplog): + n1 = UtilityNode( + description="junk", shortname="J", uuid="775e46e5-2dd4-4e34-add6-bb8c0626627d" + ) + n2 = DecisionNode( + description="junky", shortname="H", uuid="66095d54-74dc-4a75-bcf4-49676a44a2a2" + ) + arc = Arc(tail=n1, head=None, label="first") + with pytest.raises(Exception) as exc_info: + arc.head = n2 + assert [r.msg for r in caplog.records] == [ + ( + "Utility node can only have other utility nodes as successor: " + "775e46e5-2dd4-4e34-add6-bb8c0626627d/66095d54-74dc-4a75-bcf4-49676a44a2a2" + ) + ] + assert str(exc_info.value) == ( + "Utility node can only have other utility nodes as successor: " + "775e46e5-2dd4-4e34-add6-bb8c0626627d/66095d54-74dc-4a75-bcf4-49676a44a2a2" + ) + + +def test_set_tail(): + n1 = UncertaintyNode(description="junk", shortname="J") + n2 = DecisionNode(description="junky", shortname="H") + arc = Arc(tail=None, head=n2, label="first") + assert arc.tail is None + assert arc.head == n2 + assert arc.label == "first" + assert arc.dtype == "informational" + arc.tail = n1 + assert arc.tail == n1 + assert arc.dtype == "informational" + + +def test_set_tail_fail(caplog): + n1 = UtilityNode( + description="junk", shortname="J", uuid="775e46e5-2dd4-4e34-add6-bb8c0626627d" + ) + n2 = DecisionNode( + description="junky", shortname="H", uuid="66095d54-74dc-4a75-bcf4-49676a44a2a2" + ) + arc = Arc(tail=None, head=n2, label="first") + with pytest.raises(Exception) as exc_info: + arc.tail = n1 + assert [r.msg for r in caplog.records] == [ + ( + "Utility node can only have other utility nodes as successor: " + "66095d54-74dc-4a75-bcf4-49676a44a2a2/775e46e5-2dd4-4e34-add6-bb8c0626627d" + ) + ] + assert str(exc_info.value) == ( + "Utility node can only have other utility nodes as successor: " + "66095d54-74dc-4a75-bcf4-49676a44a2a2/775e46e5-2dd4-4e34-add6-bb8c0626627d" + ) diff --git a/api/tests/v0/services/classes/test_decision_tree.py b/api/tests/v0/services/classes/test_decision_tree.py new file mode 100644 index 0000000..fb5d5f2 --- /dev/null +++ b/api/tests/v0/services/classes/test_decision_tree.py @@ -0,0 +1,133 @@ +import networkx as nx +import pytest + +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.decision_tree import DecisionTree +from src.v0.services.classes.node import ( + DecisionNode, + UncertaintyNode, + UtilityNode, +) + + +@pytest.fixture +def simple_graph(): + n0 = UncertaintyNode(description="Uncertainty node 0", shortname="u0") + n1 = UncertaintyNode(description="Uncertainty node 1", shortname="u1") + n2 = UncertaintyNode(description="Uncertainty node 2", shortname="u2") + n3 = DecisionNode(description="Decision node 0", shortname="d0") + n4 = DecisionNode(description="Decision node 1", shortname="d1") + n5 = DecisionNode(description="Decision node 2", shortname="d2") + n6 = DecisionNode(description="Decision node 3", shortname="d3") + n7 = DecisionNode(description="Decision node 4", shortname="d4") + n8 = UtilityNode(description="Utility node 0", shortname="v0") + n9 = UtilityNode(description="Utility node 1", shortname="v1") + n10 = UtilityNode(description="Utility node 2", shortname="v2") + n11 = UtilityNode(description="Utility node 3", shortname="v3") + n12 = UncertaintyNode(description="Uncertainty node 3", shortname="u3") + n13 = UtilityNode(description="Utility node 4", shortname="v4") + n14 = UtilityNode(description="Utility node 5", shortname="v5") + n15 = UtilityNode(description="Utility node 6", shortname="v6") + n16 = UtilityNode(description="Utility node 7", shortname="v7") + n17 = UtilityNode(description="Utility node 8", shortname="v8") + n18 = UtilityNode(description="Utility node 9", shortname="v9") + n19 = UtilityNode(description="Utility node 10", shortname="v10") + n20 = UtilityNode(description="Utility node 11", shortname="v11") + n21 = UtilityNode(description="Utility node 12", shortname="v12") + + e0 = Arc(tail=n0, head=n1, label="e0") + e1 = Arc(tail=n1, head=n3, label="e1") + e2 = Arc(tail=n3, head=n8, label="e2") + e3 = Arc(tail=n3, head=n9, label="e3") + e4 = Arc(tail=n1, head=n4, label="e4") + e5 = Arc(tail=n4, head=n10, label="e5") + e6 = Arc(tail=n4, head=n11, label="e6") + e7 = Arc(tail=n4, head=n12, label="e7") + e8 = Arc(tail=n12, head=n20, label="e8") + e9 = Arc(tail=n12, head=n21, label="e9") + e10 = Arc(tail=n0, head=n2, label="e10") + e11 = Arc(tail=n2, head=n5, label="e11") + e12 = Arc(tail=n5, head=n13, label="e12") + e13 = Arc(tail=n5, head=n14, label="e13") + e14 = Arc(tail=n2, head=n6, label="e14") + e15 = Arc(tail=n6, head=n15, label="e15") + e16 = Arc(tail=n6, head=n16, label="e16") + e17 = Arc(tail=n2, head=n7, label="e17") + e18 = Arc(tail=n7, head=n17, label="e18") + e19 = Arc(tail=n7, head=n18, label="e19") + e20 = Arc(tail=n7, head=n19, label="e20") + + return { + "nodes": [ + n0, + n1, + n2, + n3, + n4, + n5, + n6, + n7, + n8, + n9, + n10, + n11, + n12, + n13, + n14, + n15, + n16, + n17, + n18, + n19, + n20, + n21, + ], + "arcs": [ + e0, + e1, + e2, + e3, + e4, + e5, + e6, + e7, + e8, + e9, + e10, + e11, + e12, + e13, + e14, + e15, + e16, + e17, + e18, + e19, + e20, + ], + } + + +def graph_to_decision_tree(graph): + decision_tree = DecisionTree() + decision_tree.add_nodes(graph["nodes"]) + decision_tree.add_arcs(graph["arcs"]) + return decision_tree + + +def test_class_DecisionTree(): + dt = DecisionTree() + assert isinstance(dt.graph, nx.DiGraph) + assert dt.root is None + root = DecisionNode(description="junk", shortname="D") + dt = DecisionTree(root=root) + assert len(list(dt.graph)) == 1 + assert dt.root == root + + +def test_parent(simple_graph): + dt = DecisionTree() + dt = graph_to_decision_tree(simple_graph) + n5 = simple_graph["nodes"][5] + n2 = simple_graph["nodes"][2] + assert dt.parent(n5) == n2 diff --git a/api/tests/v0/services/classes/test_discrete_conditional_probability.py b/api/tests/v0/services/classes/test_discrete_conditional_probability.py new file mode 100644 index 0000000..8b6c19a --- /dev/null +++ b/api/tests/v0/services/classes/test_discrete_conditional_probability.py @@ -0,0 +1,94 @@ +import numpy as np +import pytest +import xarray as xr + +from src.v0.services.classes.discrete_conditional_probability import ( + DiscreteConditionalProbability, +) + + +@pytest.fixture +def cpt_2d(): + values = np.array([[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]]) + conditioned_variable = {"A": ["yes", "no"]} + conditioning_variable = {"B": ["low", "mid", "high"]} + return DiscreteConditionalProbability( + probability_function=values, + variables=conditioned_variable | conditioning_variable, + ) + + +def test_class_categorical_conditional_probability(cpt_2d): + assert isinstance(cpt_2d._cpt, xr.DataArray) + np.testing.assert_equal(cpt_2d._cpt.sel(A="yes"), np.array([0.1, 0.7, 0.6])) + np.testing.assert_equal(cpt_2d._cpt.sel(A="no"), np.array([0.9, 0.3, 0.4])) + np.testing.assert_equal(cpt_2d._cpt.sel(A="yes").sel(B="mid"), np.array([0.7])) + + +def test_outcomes(): + values = np.random.random((2, 3 * 3)) + for k in range(9): + values[:, k] = values[:, k] / np.sum(values[:, k]) + values = np.reshape(values, (2, 3, 3)) + conditioned_variables = {"A": ["yes", "no"]} + conditioning_variable = {"B": ["R", "G", "B"], "C": ["low", "mid", "high"]} + cpt = DiscreteConditionalProbability( + probability_function=values, + variables=conditioned_variables | conditioning_variable, + ) + assert cpt.outcomes == (("yes", "no")) + + +def test_outcomes_1d(cpt_2d): + assert cpt_2d.outcomes == ("yes", "no") + + +def test_variables(cpt_2d): + assert cpt_2d.variables == ("A", "B") + + +def test_conditioned_variables(cpt_2d): + assert cpt_2d.conditioned_variables == ("A",) + + +def test_conditioning_variables(cpt_2d): + assert cpt_2d.conditioning_variables == ("B",) + + +def test_initialize_nan(): + conditioned_variable = {"a": np.array([1, 2, 3])} + conditioning_variables = {"b": np.array([4, 5]), "c": ["yes", "no"]} + result = DiscreteConditionalProbability.initialize_nan( + conditioned_variable, conditioning_variables=conditioning_variables + ) + assert result._cpt.shape == (3, 2, 2) + assert np.all(np.isnan(result._cpt)) + + +# def test_initialize_nan_fail(): +# conditioned_variable = { +# "a": np.array([1, 2, 3]), +# "b": np.array([[4, 5, 6], [7, 8, 9]]), +# "c": ["yes", "no"] +# } +# with pytest.raises(Exception) as exc: +# DiscreteConditionalProbability.initialize_nan(variables=conditioned_variable) +# assert str(exc.value) == "One of the variables cannot be interpreted as 1D" + + +def test_initialize_uniform(): + conditioned_variable = {"a": np.array([1, 2])} + conditioning_variables = {"b": np.array([4, 5, 3]), "c": ["yes", "no"]} + result = DiscreteConditionalProbability.initialize_uniform( + conditioned_variables=conditioned_variable, + conditioning_variables=conditioning_variables, + ) + assert result._cpt.shape == (2, 3, 2) + assert np.all(result._cpt == 0.5) + + +def test_get_distribution(cpt_2d): + np.testing.assert_allclose( + cpt_2d.get_distribution(A="yes"), np.array([0.1, 0.7, 0.6]) + ) + assert cpt_2d.get_distribution(A="no", B="mid") == 0.3 diff --git a/api/tests/v0/services/classes/test_discrete_unconditional_probability.py b/api/tests/v0/services/classes/test_discrete_unconditional_probability.py new file mode 100644 index 0000000..34a00ce --- /dev/null +++ b/api/tests/v0/services/classes/test_discrete_unconditional_probability.py @@ -0,0 +1,75 @@ +import numpy as np +import pytest +import xarray as xr + +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) + + +@pytest.fixture +def cpt_2d(): + values = np.array([[0.1, 0.3, 0.2], [0.2, 0.1, 0.1]]) + coords = {"A": ["yes", "no"], "B": ["low", "mid", "high"]} + return DiscreteUnconditionalProbability(values, coords) + + +def test_class_categorical_conditional_probability(cpt_2d): + assert isinstance(cpt_2d._cpt, xr.DataArray) + np.testing.assert_equal(cpt_2d._cpt.sel(A="yes"), np.array([0.1, 0.3, 0.2])) + np.testing.assert_equal(cpt_2d._cpt.sel(A="no"), np.array([0.2, 0.1, 0.1])) + np.testing.assert_equal(cpt_2d._cpt.sel(A="yes").sel(B="mid"), np.array([0.3])) + + +def test_outcomes(cpt_2d): + assert cpt_2d.outcomes == ( + "yes - low", + "yes - mid", + "yes - high", + "no - low", + "no - mid", + "no - high", + ) + + +def test_outcomes_1d(): + values = np.array([0.1, 0.9]) + coords = {"A": ["yes", "no"]} + probability = DiscreteUnconditionalProbability(values, coords) + assert probability.outcomes == ("yes", "no") + + +def test_variables(cpt_2d): + assert cpt_2d.variables == ("A", "B") + + +def test_initialize_nan(): + coords = {"a": np.array([1, 2, 3]), "b": np.array([4, 5]), "c": ["yes", "no"]} + result = DiscreteUnconditionalProbability.initialize_nan(variables=coords) + assert result._cpt.shape == (3, 2, 2) + assert np.all(np.isnan(result._cpt)) + + +# def test_initialize_nan_fail(): +# coords = { +# "a": np.array([1, 2, 3]), +# "b": np.array([[4, 5, 6], [7, 8, 9]]), +# "c": ["yes", "no"] +# } +# with pytest.raises(Exception) as exc: +# DiscreteUnconditionalProbability.initialize_nan(variables=coords) +# assert str(exc.value) == "One of the variables cannot be interpreted as 1D." + + +def test_initialize_uniform(): + coords = {"a": np.array([1, 2, 3]), "b": np.array([4, 5]), "c": ["yes", "no"]} + result = DiscreteUnconditionalProbability.initialize_uniform(variables=coords) + assert result._cpt.shape == (3, 2, 2) + assert np.all(np.linalg.norm(result._cpt - 1 / 12) < 1e-12) + + +def test_get_distribution(cpt_2d): + np.testing.assert_allclose( + cpt_2d.get_distribution(A="yes"), np.array([0.1, 0.3, 0.2]) + ) + assert cpt_2d.get_distribution(A="no", B="mid") == 0.1 diff --git a/api/tests/v0/services/classes/test_influence_diagram.py b/api/tests/v0/services/classes/test_influence_diagram.py new file mode 100644 index 0000000..83bb87e --- /dev/null +++ b/api/tests/v0/services/classes/test_influence_diagram.py @@ -0,0 +1,195 @@ +import networkx as nx +import pytest + +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.influence_diagram import InfluenceDiagram +from src.v0.services.classes.node import ( + DecisionNode, + UncertaintyNode, + UtilityNode, +) + + +@pytest.fixture +def simple_graph(): + n0 = UncertaintyNode(description="Uncertainty node 1", shortname="u1") + n1 = UncertaintyNode(description="Uncertainty node 2", shortname="u2") + n2 = UncertaintyNode(description="Uncertainty node 3", shortname="u3") + n3 = UncertaintyNode(description="Uncertainty node 4", shortname="u4") + n4 = DecisionNode(description="Decision node 1", shortname="d1") + n5 = UncertaintyNode(description="Uncertainty node 5", shortname="u5") + n6 = DecisionNode(description="Decision node 2", shortname="d2") + n7 = UncertaintyNode(description="Uncertainty node 6", shortname="u6") + n8 = UncertaintyNode(description="Uncertainty node 7", shortname="u7") + n9 = UncertaintyNode(description="Uncertainty node 8", shortname="u8") + n10 = UtilityNode(description="Utility node 1", shortname="v1") + + e0 = Arc(tail=n0, head=n4, label="e0") + e1 = Arc(tail=n1, head=n4, label="e1") + e2 = Arc(tail=n2, head=n4, label="e2") + e3 = Arc(tail=n3, head=n6, label="e3") + e4 = Arc(tail=n4, head=n6, label="e4") + e5 = Arc(tail=n4, head=n5, label="e5") + e6 = Arc(tail=n6, head=n7, label="e6") + e7 = Arc(tail=n6, head=n8, label="e7") + e8 = Arc(tail=n6, head=n9, label="e8") + e9 = Arc(tail=n5, head=n10, label="e9") + + return { + "nodes": [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10], + "arcs": [e0, e1, e2, e3, e4, e5, e6, e7, e8, e9], + } + + +def graph_to_influence_diagram(graph): + influence_diagram = InfluenceDiagram() + influence_diagram.add_nodes(graph["nodes"]) + influence_diagram.add_arcs(graph["arcs"]) + return influence_diagram + + +def test_class_InfluenceDiagram(): + ID = InfluenceDiagram() + assert isinstance(ID.graph, nx.DiGraph) + + +def test_copy(): + ID = InfluenceDiagram() + assert nx.utils.graphs_equal(ID.graph, ID.copy().graph) + + +def test_is_acyclic_true(simple_graph): + ID = InfluenceDiagram() + ID = graph_to_influence_diagram(simple_graph) + assert ID.is_acyclic + + +def test_is_acyclic_false(): + n0 = UncertaintyNode(description="Uncertainty node 1", shortname="u1") + n1 = UncertaintyNode(description="Uncertainty node 2", shortname="u2") + n2 = UncertaintyNode(description="Uncertainty node 3", shortname="u3") + + e0 = Arc(tail=n0, head=n1, label="e0") + e1 = Arc(tail=n1, head=n2, label="e1") + + ID = InfluenceDiagram() + ID = graph_to_influence_diagram({"nodes": [n0, n1, n2], "arcs": [e0, e1]}) + assert ID.is_acyclic + + +def test_nodes(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + assert len(ID.nodes) == 11 + assert ID.nodes[0].description == "Uncertainty node 1" + + +def test_arcs(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + assert len(ID.arcs) == 10 + assert ID.arcs[0].tail.description == "Uncertainty node 1" + assert ID.arcs[0].label == "e0" + + +def test_node_uuids(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + result = ID.node_uuids + assert len(result) == 11 + assert set(result) == {item.uuid for item in simple_graph["nodes"]} + + +def test_node_in(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + assert ID.node_in(simple_graph["nodes"][0]) + assert not ID.node_in( + UncertaintyNode(description="Uncertainty node 1", shortname="u1") + ) # the created node should have a new uuid, so not in the graph + + +def test_get_parents(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + n4 = simple_graph["nodes"][4] + result = ID.get_parents(n4) + target = simple_graph["nodes"][0:3] + assert result == target + + +def test_get_parents_fail(caplog, simple_graph): + ID = graph_to_influence_diagram(simple_graph) + with pytest.raises(Exception) as exc_info: + ID.get_parents(DecisionNode(description="junk", shortname="D")) + assert all("The node is not in the graph:" in r.msg for r in caplog.records) + assert "The node is not in the graph:" in str(exc_info.value) + + +def test_get_children(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + n6 = simple_graph["nodes"][6] + result = ID.get_children(n6) + target = simple_graph["nodes"][7:10] + assert result == target + + n4 = simple_graph["nodes"][4] + result = ID.get_children(n4) + target = [simple_graph["nodes"][5], simple_graph["nodes"][6]] + assert all(item in target for item in result) + assert all(item in result for item in target) + + +def test_get_children_fail(caplog, simple_graph): + ID = graph_to_influence_diagram(simple_graph) + with pytest.raises(Exception) as exc_info: + ID.get_children(DecisionNode(description="junk", shortname="D")) + assert all("The node is not in the graph:" in r.msg for r in caplog.records) + assert "The node is not in the graph:" in str(exc_info.value) + + +def test_get_decision_nodes(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + assert ID.get_decision_nodes() == [ + simple_graph["nodes"][4], + simple_graph["nodes"][6], + ] + + +def test_get_utility_nodes(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + assert ID.get_utility_nodes() == [simple_graph["nodes"][10]] + + +def test_get_uncertainty_nodes(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + assert ID.get_uncertainty_nodes() == [ + simple_graph["nodes"][0], + simple_graph["nodes"][1], + simple_graph["nodes"][2], + simple_graph["nodes"][3], + simple_graph["nodes"][5], + simple_graph["nodes"][7], + simple_graph["nodes"][8], + simple_graph["nodes"][9], + ] + + +def test_nodes_count(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + assert ID.decision_count == 2 + assert ID.utility_count == 1 + assert ID.uncertainty_count == 8 + + +def test_has_children(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + n4 = simple_graph["nodes"][4] + n8 = simple_graph["nodes"][8] + assert ID.has_children(n4) + assert not ID.has_children(n8) + + +def test_get_node_from_uuid(simple_graph): + ID = graph_to_influence_diagram(simple_graph) + n4 = simple_graph["nodes"][4] + uuid = n4.uuid + node = ID.get_node_from_uuid(uuid) + assert isinstance(node, DecisionNode) + assert node.description == "Decision node 1" + assert node.shortname == "d1" diff --git a/api/tests/v0/services/classes/test_node.py b/api/tests/v0/services/classes/test_node.py new file mode 100644 index 0000000..eb59749 --- /dev/null +++ b/api/tests/v0/services/classes/test_node.py @@ -0,0 +1,114 @@ +import numpy as np + +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) +from src.v0.services.classes.node import ( + DecisionNode, + UncertaintyNode, + UtilityNode, +) + + +def test_class_DecisionNode(): + node = DecisionNode(description="junk", shortname="J") + assert node.description == "junk" + assert node.shortname == "J" + assert isinstance(node.uuid, str) + assert len(node.uuid) == 36 + assert isinstance(node.alternatives, list) and not node.alternatives + assert isinstance(node.states, list) and not node.states + assert node.is_decision_node + assert not node.is_uncertainty_node + assert not node.is_utility_node + + +def test_class_DecisionNode_setter_alternatives_as_list(): + node = DecisionNode(description="junk", shortname="J") + node.description = "C2H5OH" + node.shortname = "ethanol" + node.uuid = None + node.alternatives = ["1", "2", "3"] + assert node.description == "C2H5OH" + assert node.shortname == "ethanol" + assert isinstance(node.uuid, str) + assert len(node.uuid) == 36 + assert node.alternatives == ["1", "2", "3"] + + +def test_class_DecisionNode_setter_alternatives_as_empty(): + node = DecisionNode(description="junk", shortname="J") + node.description = "C2H5OH" + node.shortname = "ethanol" + node.uuid = None + node.alternatives = [] + assert node.description == "C2H5OH" + assert node.shortname == "ethanol" + assert isinstance(node.uuid, str) + assert len(node.uuid) == 36 + assert node.alternatives == [] + + +def test_class_DecisionNode_setter_alternatives_as_tuple(): + node = DecisionNode(description="junk", shortname="J") + node.description = "C2H5OH" + node.shortname = "ethanol" + node.uuid = None + node.alternatives = ("1", "2", "3") + assert node.description == "C2H5OH" + assert node.shortname == "ethanol" + assert isinstance(node.uuid, str) + assert len(node.uuid) == 36 + assert node.alternatives == ["1", "2", "3"] + + +def test_class_UncertaintyNode(): + node = UncertaintyNode(description="junk", shortname="J") + assert node.description == "junk" + assert node.shortname == "J" + assert isinstance(node.uuid, str) + assert len(node.uuid) == 36 + assert node.probability is None + assert isinstance(node.outcomes, tuple) and not node.outcomes + assert isinstance(node.states, tuple) and not node.states + assert not node.is_decision_node + assert node.is_uncertainty_node + assert not node.is_utility_node + + +def test_class_UncertaintyNode_setter(): + node = UncertaintyNode(description="junk", shortname="J") + node.probability = DiscreteUnconditionalProbability( + probability_function=np.array([1, 0]), variables={"outcome": ["y", "n"]} + ) + assert node.outcomes == ("y", "n") + + +def test_class_UtilityNode(): + node = UtilityNode(description="junk", shortname="J") + assert node.description == "junk" + assert node.shortname == "J" + assert isinstance(node.uuid, str) + assert len(node.uuid) == 36 + assert node.states == [] + assert not node.is_decision_node + assert not node.is_uncertainty_node + assert node.is_utility_node + node.utility = [] + assert node.states == [] + + +def test_class_UtilityNode_setter(): + node = UtilityNode(description="junk", shortname="J") + assert isinstance(node, UtilityNode) + + +def test_copy(): + node = DecisionNode( + description="junk", shortname="J", alternatives=["a0", "a1", "a2"] + ) + copied_node = node.copy() + assert isinstance(copied_node, type(node)) + assert copied_node.description == node.description + assert copied_node.shortname == node.shortname + assert copied_node.alternatives == node.alternatives diff --git a/api/tests/v0/services/format_conversions/__init__.py b/api/tests/v0/services/format_conversions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/tests/v0/services/format_conversions/test_arc.py b/api/tests/v0/services/format_conversions/test_arc.py new file mode 100644 index 0000000..7fb331b --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_arc.py @@ -0,0 +1,94 @@ +import json + +import pytest + +from src.v0.services.classes.arc import Arc +from src.v0.services.format_conversions import arc, node + +TESTDATA = "v0/services/testdata" + + +@pytest.fixture +def influence_diagram(copy_testdata_tmpdir, tmp_path): + copy_testdata_tmpdir(TESTDATA) + with open(tmp_path / "simple_id.json") as f: + data = json.load(f) + issues = data["vertices"]["issues"] + issues = [ + {"uuid" if k == "id" else k: v for k, v in issue.items()} for issue in issues + ] + data = { + "issues": issues, + "edges": [edge for edge in data["edges"] if edge["label"] == "influences"], + } + return data + + +def test_class_ArcConversion_from_json_fail_not_head(caplog): + as_json = { + "id": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + "outV": "98e3d193-d830-452f-9fe8-c21d258ef603", + "inV": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", + "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + "label": "influences", + } + issues = [ + { + "description": "Joe does not know the state of the car", + "uuid": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", + }, + ] + with pytest.raises(Exception) as exc_info: + arc.ArcConversion().from_json(as_json, issues) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an Arc: ([], [])" + ] + assert str(exc_info.value) == "Data cannot be used to create an Arc: ([], [])" + + +def test_class_ArcConversion_from_json_fail_not_edge(caplog): + as_json = { + "id": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + "label": "influences", + } + issues = [ + { + "description": "Joe does not know the state of the car", + "uuid": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", + }, + ] + with pytest.raises(Exception) as exc_info: + arc.ArcConversion().from_json(as_json, issues) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an Arc: ['id', 'uuid', 'label']" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create an Arc: ['id', 'uuid', 'label']" + ) + + +def test_class_ArcConversion_from_json(influence_diagram): + # First "influences" edge is actually b -> v + result = arc.ArcConversion().from_json( + influence_diagram["edges"][0], influence_diagram["issues"] + ) + assert isinstance(result, Arc) + assert result.tail.description == "b" + assert result.head.description == "v" + + +def test_class_ArcConversion_to_json(influence_diagram): + # Arc b->v (issues 0 and 2) + tail = node.InfluenceDiagramNodeConversion().from_json( + influence_diagram["issues"][0] + ) + head = node.InfluenceDiagramNodeConversion().from_json( + influence_diagram["issues"][2] + ) + edge = Arc(tail=tail, head=head) + result = arc.ArcConversion().to_json(edge, [tail, head]) + assert result["outV"] == "b1f3d18d-20a8-475b-981f-32449d1ee6bd" + assert result["inV"] == "8106127e-ab4a-4aa5-a865-8b55789e7d7a" + assert result["uuid"] == edge.uuid diff --git a/api/tests/v0/services/format_conversions/test_base.py b/api/tests/v0/services/format_conversions/test_base.py new file mode 100644 index 0000000..f212dc2 --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_base.py @@ -0,0 +1,30 @@ +import pytest + +from src.v0.models.meta import EdgeMetaDataResponse, VertexMetaDataResponse +from src.v0.services.format_conversions.base import ConversionABC, MetadataCreate + + +def test_class_ConversionABC(monkeypatch): + monkeypatch.setattr( + ConversionABC, + "__abstractmethods__", + set(), + ) + conversion = ConversionABC() + with pytest.raises(NotImplementedError): + conversion.from_json(None) + with pytest.raises(NotImplementedError): + conversion.to_json(None) + + +def test_MetadataCreate_vertex(): + metadata = MetadataCreate.vertex("1") + assert isinstance(metadata, VertexMetaDataResponse) + assert metadata.uuid == "1" + assert metadata.version == "v0" + + +def test_MetadataCreate_edge(): + metadata = MetadataCreate.edge("1") + assert isinstance(metadata, EdgeMetaDataResponse) + assert metadata.uuid == "1" diff --git a/api/tests/v0/services/format_conversions/test_decision_node.py b/api/tests/v0/services/format_conversions/test_decision_node.py new file mode 100644 index 0000000..12b6a5f --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_decision_node.py @@ -0,0 +1,80 @@ +from copy import deepcopy + +import pytest + +from src.v0.services.classes.node import DecisionNode +from src.v0.services.format_conversions.node import DecisionNodeConversion + + +@pytest.fixture +def decision_node(): + return { + "description": "testing node", + "shortname": "Node", + "boundary": "in", + "comments": [{"author": "Jr.", "comment": "Nope"}], + "category": "Decision", + "decisionType": "Focus", + "alternatives": ["yes", "no"], + "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + } + + +def test_class_DecisionNodeConversion_from_json_fail(caplog): + as_json = { + "category": "Junk", + "description": "C2H5OH", + "shortname": "veni vidi vici", + "alternatives": None, + } + with pytest.raises(Exception) as exc_info: + DecisionNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create a DecisionNode: Junk" + ] + assert str(exc_info.value) == "Data cannot be used to create a DecisionNode: Junk" + + +def test_DecisionNodeConversion_from_json(decision_node): + result = DecisionNodeConversion().from_json(decision_node) + assert isinstance(result, DecisionNode) + assert result.description == "testing node" + assert result.shortname == "Node" + assert result.uuid == "a6ab145e-2ca9-49e2-8c4f-9607688e57a9" + assert result.alternatives == ["yes", "no"] + + +def test_DecisionNodeConversion_from_json_no_alternatives(decision_node): + local_node = deepcopy(decision_node) + local_node["alternatives"] = None + result = DecisionNodeConversion().from_json(local_node) + assert isinstance(result, DecisionNode) + assert result.description == "testing node" + assert result.shortname == "Node" + assert result.alternatives == [] + + +def test_DecisionNodeConversion_to_json(decision_node): + data = DecisionNode( + description=decision_node["description"], + shortname=decision_node["shortname"], + alternatives=decision_node["alternatives"], + ) + result = DecisionNodeConversion().to_json(data) + assert result["description"] == decision_node["description"] + assert result["shortname"] == decision_node["shortname"] + assert result["decisionType"] == "Focus" + assert result["uuid"] == data.uuid + assert result["alternatives"] == decision_node["alternatives"] + + +def test_DecisionNodeConversion_to_json_no_alternatives(decision_node): + data = DecisionNode( + description=decision_node["description"], shortname=decision_node["shortname"] + ) + result = DecisionNodeConversion().to_json(data) + assert result["shortname"] == decision_node["shortname"] + assert result["description"] == decision_node["description"] + assert result["decisionType"] == "Focus" + assert result["uuid"] == data.uuid + assert result["alternatives"] is None diff --git a/api/tests/v0/services/format_conversions/test_directed_graph.py b/api/tests/v0/services/format_conversions/test_directed_graph.py new file mode 100644 index 0000000..d64953c --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_directed_graph.py @@ -0,0 +1,228 @@ +import json + +import pytest + +from src.v0.services.classes.arc import Arc +from src.v0.services.classes.decision_tree import DecisionTree +from src.v0.services.classes.influence_diagram import InfluenceDiagram +from src.v0.services.classes.node import DecisionNode +from src.v0.services.format_conversions import arc, directed_graph, node + +TESTDATA = "v0/services/testdata" + + +@pytest.fixture +def influence_diagram(copy_testdata_tmpdir, tmp_path): + copy_testdata_tmpdir(TESTDATA) + with open(tmp_path / "used_car_buyer_problem.json") as f: + data = json.load(f) + issues = data["vertices"]["issues"] + issues = [ + {"uuid" if k == "id" else k: v for k, v in issue.items()} for issue in issues + ] + data = { + "vertices": issues, + "edges": [edge for edge in data["edges"] if edge["label"] == "influences"], + } + return data + + +def test_class_InfluenceDiagramConversion_from_json_fail_no_vertex(caplog): + as_json = { + "type": "Junk", + "name": "C2H5OH", + "shortname": "veni vidi vici", + "alternatives": None, + } + with pytest.raises(Exception) as exc_info: + directed_graph.InfluenceDiagramConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an InfluenceDiagram: None" + ] + assert ( + str(exc_info.value) == "Data cannot be used to create an InfluenceDiagram: None" + ) + + +def test_class_InfluenceDiagramConversion_from_json_fail_no_id_nodes(caplog): + as_json = { + "vertices": [ + { + "tag": ["State"], + "type": "junk", + "shortname": "State", + "description": "Issue description", + } + ] + } + + with pytest.raises(Exception) as exc_info: + directed_graph.InfluenceDiagramConversion().from_json(as_json) + assert "Data cannot be used to create an InfluenceDiagram: None" in [ + r.msg for r in caplog.records + ] + assert ( + str(exc_info.value) == "Data cannot be used to create an InfluenceDiagram: None" + ) + + +def test_class_InfluenceDiagramConversion_from_json_fail_no_focus_decision(caplog): + as_json = { + "vertices": [ + { + "tag": ["State"], + "type": "Decision", + "shortname": "State", + "description": "Issue description", + "keyUncertainty": "Key", + "alternatives": ["Peach", "Lemon"], + } + ] + } + + with pytest.raises(Exception) as exc_info: + directed_graph.InfluenceDiagramConversion().from_json(as_json) + assert "Data cannot be used to create an InfluenceDiagram: None" in [ + r.msg for r in caplog.records + ] + assert ( + str(exc_info.value) == "Data cannot be used to create an InfluenceDiagram: None" + ) + + +def test_class_InfluenceDiagramConversion_from_json_fail_no_key_uncertainty(caplog): + as_json = { + "vertices": [ + { + "tag": ["State"], + "type": "Uncertainty", + "shortname": "State", + "description": "Issue description", + "probabilities": { + "type": "DiscreteUnconditionalProbability", + "probability_function": [[0.8, 0.2]], + "variables": {"State": ["Peach", "Lemon"]}, + }, + } + ] + } + + with pytest.raises(Exception) as exc_info: + directed_graph.InfluenceDiagramConversion().from_json(as_json) + assert "Data cannot be used to create an InfluenceDiagram: None" in [ + r.msg for r in caplog.records + ] + assert ( + str(exc_info.value) == "Data cannot be used to create an InfluenceDiagram: None" + ) + + +def test_class_InfluenceDiagramConversion_from_json_fail_badly_formatted_node(caplog): + as_json = { + "vertices": [ + { + "tag": ["State"], + "type": "Uncertainty", + "index": "0", + } + ] + } + + with pytest.raises(Exception) as exc_info: + directed_graph.InfluenceDiagramConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an influence diagram Node: category: None", + "Data cannot be used to create an InfluenceDiagram: None", + ] + assert ( + str(exc_info.value) == "Data cannot be used to create an InfluenceDiagram: None" + ) + + +def test_DecisionNodeConversion_from_json(influence_diagram): + result = directed_graph.InfluenceDiagramConversion().from_json(influence_diagram) + assert isinstance(result, InfluenceDiagram) + assert result.decision_count == 2 + assert result.uncertainty_count == 2 + assert result.utility_count == 1 + + +def test_DecisionNodeConversion_to_json(influence_diagram): + diagram = InfluenceDiagram() + diagram.add_nodes( + [ + node.InfluenceDiagramNodeConversion().from_json(item) + for item in influence_diagram["vertices"] + ] + ) + diagram.add_arcs( + [ + arc.ArcConversion().from_json(item, influence_diagram["vertices"]) + for item in influence_diagram["edges"] + ] + ) + result = directed_graph.InfluenceDiagramConversion().to_json(diagram) + assert len(result["vertices"]) == 5 + assert len(result["edges"]) == 6 + assert result["vertices"][1]["category"] == "Decision" + assert result["edges"][1]["label"] == "influences" + + +def test_DecisionTreeConversion(): + with pytest.raises(NotImplementedError): + directed_graph.DecisionTreeConversion().from_json(None) + + decision1 = DecisionNode( + shortname="D1", description="", uuid="11111111-9999-4444-9999-aaaaaaaaaaaa" + ) + decision2 = DecisionNode( + shortname="D2", description="", uuid="22222222-9999-4444-9999-bbbbbbbbbbbb" + ) + dt = DecisionTree() + dt.add_nodes((decision1, decision2)) + dt.add_arc(Arc(tail=decision1, head=decision2, label="branch")) + + assert directed_graph.DecisionTreeConversion().to_json(dt) == { + "id": { + "node_type": "Decision", + "shortname": "D1", + "description": "", + "branch_name": "", + "alternatives": None, + "probabilities": None, + "utility": None, + "uuid": "11111111-9999-4444-9999-aaaaaaaaaaaa", + }, + "children": [ + { + "id": { + "node_type": "Decision", + "shortname": "D2", + "description": "", + "branch_name": "branch", + "alternatives": None, + "probabilities": None, + "utility": None, + "uuid": "22222222-9999-4444-9999-bbbbbbbbbbbb", + } + } + ], + } + + +def test_DecisionTreeConversion_fail_no_root(caplog): + decision1 = DecisionNode( + shortname="D1", description="", uuid="11111111-9999-4444-9999-aaaaaaaaaaaa" + ) + decision2 = DecisionNode( + shortname="D2", description="", uuid="22222222-9999-4444-9999-bbbbbbbbbbbb" + ) + dt = DecisionTree() + dt.add_nodes((decision1, decision2)) + + with pytest.raises(Exception) as exc_info: + directed_graph.DecisionTreeConversion().to_json(dt) + assert [r.msg for r in caplog.records] == [ + "Decision tree has no defined root node: None" + ] + assert str(exc_info.value) == "Decision tree has no defined root node: None" diff --git a/api/tests/v0/services/format_conversions/test_node.py b/api/tests/v0/services/format_conversions/test_node.py new file mode 100644 index 0000000..b4f7304 --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_node.py @@ -0,0 +1,251 @@ +import numpy as np +import pytest + +from src.v0.services.classes.node import DecisionNode, UncertaintyNode, UtilityNode +from src.v0.services.format_conversions.node import ( + DecisionJSONConversion, + InfluenceDiagramNodeConversion, + UncertaintyJSONConversion, + add_metadata, +) +from src.v0.services.format_conversions.probability import ProbabilityConversion + + +@pytest.fixture +def node(): + return { + "description": "testing node", + "shortname": "Node", + "boundary": "in", + "comments": [{"author": "Jr.", "comment": "Nope"}], + "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + } + + +@pytest.fixture +def decision_node(node): + return node | { + "category": "Decision", + "decisionType": "Focus", + "alternatives": ["yes", "no"], + } + + +@pytest.fixture +def uncertainty_node(node): + return node | { + "category": "Uncertainty", + "keyUncertainty": "true", + "probabilities": { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.3], [0.7]], + "variables": {"States": ["s1", "s2"]}, + }, + } + + +@pytest.fixture +def value_metric_node(node): + return node | {"category": "Value Metric"} + + +def test_add_metadata(): + metadata = add_metadata("1") + assert metadata["uuid"] == "1" + + +def test_DecisionJSONConversion(decision_node): + node = InfluenceDiagramNodeConversion().from_json(decision_node) + assert DecisionJSONConversion().states(node) == ["yes", "no"] + assert DecisionJSONConversion().decision_type(node) == "Focus" + + +def test_UncertaintyJSONConversion(uncertainty_node): + node = InfluenceDiagramNodeConversion().from_json(uncertainty_node) + assert UncertaintyJSONConversion().probability(node) == { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.3], [0.7]], + "variables": {"States": ["s1", "s2"]}, + } + assert UncertaintyJSONConversion().key_uncertainty(node) == "true" + assert UncertaintyJSONConversion().source(node) == "" + + +def test_InfluenceDiagramNodeConversion_from_json_fail_not_id_node_due_to_category( + caplog, +): + as_json = { + "category": "Junk", + "boundary": "in", + "description": "C2H5OH", + "shortname": "veni vidi vici", + "consequences": None, + } + with pytest.raises(Exception) as exc_info: + InfluenceDiagramNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an influence diagram Node: category: Junk" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create an influence diagram Node: category: Junk" + ) + + +def test_InfluenceDiagramNodeConversion_from_json_fail_not_id_node_due_to_no_shortname( + caplog, +): + as_json = { + "category": "Decision", + "boundary": "in", + "description": "C2H5OH", + "consequences": None, + } + with pytest.raises(Exception) as exc_info: + InfluenceDiagramNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an influence diagram Node: shortname: None" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create an influence diagram Node: shortname: None" + ) + + +def test_InfluenceDiagramNodeConversion_from_json_fail_not_focus_decision(caplog): + as_json = { + "category": "Decision", + "boundary": "in", + "decisionType": "junk", + "description": "C2H5OH", + "shortname": "veni vidi vici", + "consequences": None, + } + with pytest.raises(Exception) as exc_info: + InfluenceDiagramNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an influence diagram Node: decisionType: junk" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create an influence diagram Node: decisionType: junk" + ) + + +def test_InfluenceDiagramNodeConversion_from_json_fail_not_key_uncertainty(caplog): + as_json = { + "category": "Uncertainty", + "boundary": "in", + "keyUncertainty": "junk", + "description": "C2H5OH", + "shortname": "veni vidi vici", + "consequences": None, + } + with pytest.raises(Exception) as exc_info: + InfluenceDiagramNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an influence diagram Node: " + "keyUncertainty: junk" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create an influence diagram Node: " + "keyUncertainty: junk" + ) + + +def test_InfluenceDiagramNodeConversion_from_json_fail_not_in_on_boundary(caplog): + as_json = { + "category": "Uncertainty", + "boundary": "out", + "keyUncertainty": "true", + "description": "C2H5OH", + "shortname": "veni vidi vici", + "consequences": None, + } + with pytest.raises(Exception) as exc_info: + InfluenceDiagramNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create an influence diagram Node: boundary: out" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create an influence diagram Node: boundary: out" + ) + + +def test_InfluenceDiagramNodeConversion_from_json_decision(decision_node): + result = InfluenceDiagramNodeConversion().from_json(decision_node) + assert isinstance(result, DecisionNode) + assert result.description == "testing node" + assert result.shortname == "Node" + assert result.alternatives == ["yes", "no"] + + +def test_InfluenceDiagramNodeConversion_from_json_uncertainty(uncertainty_node): + result = InfluenceDiagramNodeConversion().from_json(uncertainty_node) + assert isinstance(result, UncertaintyNode) + assert result.description == "testing node" + assert result.shortname == "Node" + assert result.probability.outcomes == ("s1", "s2") + assert result.probability.variables == ("States",) + np.testing.assert_allclose( + result.probability.get_distribution(), np.array([0.3, 0.7]) + ) + + +def test_InfluenceDiagramNodeConversion_from_json(value_metric_node): + result = InfluenceDiagramNodeConversion().from_json(value_metric_node) + assert isinstance(result, UtilityNode) + assert result.description == "testing node" + assert result.shortname == "Node" + + +def test_InfluenceDiagramNodeConversion_to_json_fail_not_influence_diagram_node(caplog): + with pytest.raises(Exception) as exc_info: + InfluenceDiagramNodeConversion().to_json(None) + assert [r.msg for r in caplog.records] == [ + "Data is not an InfluenceDiagram Node: None" + ] + assert str(exc_info.value) == "Data is not an InfluenceDiagram Node: None" + + +def test_InfluenceDiagramNodeConversion_to_json_decision(decision_node): + data = DecisionNode( + description=decision_node["description"], + shortname=decision_node["shortname"], + uuid=decision_node["uuid"], + alternatives=decision_node["alternatives"], + ) + result = InfluenceDiagramNodeConversion().to_json(data) + assert result["description"] == "testing node" + assert result["shortname"] == "Node" + assert result["uuid"] == data.uuid + assert result["alternatives"] == ["yes", "no"] + + +def test_InfluenceDiagramNodeConversion_to_json_uncertainty(uncertainty_node): + probabilities = ProbabilityConversion().from_json(uncertainty_node["probabilities"]) + data = UncertaintyNode( + description=uncertainty_node["description"], + shortname=uncertainty_node["shortname"], + uuid=uncertainty_node["uuid"], + probability=probabilities, + ) + result = InfluenceDiagramNodeConversion().to_json(data) + assert result["description"] == uncertainty_node["description"] + assert result["shortname"] == uncertainty_node["shortname"] + assert result["keyUncertainty"] == "true" + assert result["uuid"] == data.uuid + assert result["probabilities"] == uncertainty_node["probabilities"] + + +def test_InfluenceDiagramNodeConversion_to_json_utility(value_metric_node): + data = UtilityNode( + description=value_metric_node["description"], + shortname=value_metric_node["shortname"], + ) + result = InfluenceDiagramNodeConversion().to_json(data) + assert result["description"] == value_metric_node["description"] + assert result["shortname"] == value_metric_node["shortname"] + assert result["uuid"] == data.uuid diff --git a/api/tests/v0/services/format_conversions/test_probability.py b/api/tests/v0/services/format_conversions/test_probability.py new file mode 100644 index 0000000..ced62d2 --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_probability.py @@ -0,0 +1,232 @@ +import numpy as np +import pytest + +from src.v0.services.classes.discrete_conditional_probability import ( + DiscreteConditionalProbability as DiscreteConditionalProbability, +) +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability as DiscreteUnconditionalProbability, +) +from src.v0.services.format_conversions.probability import ( + DiscreteConditionalProbabilityConversion, + DiscreteUnconditionalProbabilityConversion, + ProbabilityConversion, +) + + +def test_class_ProbabilityConversion_from_json_fail_not_dict(caplog): + with pytest.raises(Exception) as exc_info: + ProbabilityConversion().from_json("None") + assert [r.msg for r in caplog.records] == ["Unreckonized probability type: None"] + assert str(exc_info.value) == "Unreckonized probability type: None" + + +def test_class_ProbabilityConversion_from_json_fail_wrong_probability(caplog): + with pytest.raises(Exception) as exc_info: + ProbabilityConversion().from_json({"type": "unknown"}) + assert [r.msg for r in caplog.records] == [ + "Unreckonized probability type: {'type': 'unknown'}" + ] + assert str(exc_info.value) == "Unreckonized probability type: {'type': 'unknown'}" + + +def test_class_ProbabilityConversion_from_json_conditional(): + as_json = { + "dtype": "DiscreteConditionalProbability", + "probability_function": [[0.4, 0.5], [0.6, 0.5]], + "variables": { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + "attributes": { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + }, + } + result = ProbabilityConversion().from_json(as_json) + assert isinstance(result, DiscreteConditionalProbability) + assert result._cpt.attrs == { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + } + assert result._cpt.coords.dims == ("Node1", "Node2") + assert result._cpt.coords["Node1"].values.tolist() == ["Outcome1", "Outcome2"] + assert result._cpt.coords["Node2"].values.tolist() == ["Outcome21", "Outcome22"] + assert result._cpt.data.tolist() == [[0.4, 0.5], [0.6, 0.5]] + + +def test_class_ProbabilityConversion_from_json_unconditional(): + as_json = { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.2, 0.4], [0.1, 0.3]], + "variables": { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + "attributes": { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + }, + } + result = ProbabilityConversion().from_json(as_json) + assert isinstance(result, DiscreteUnconditionalProbability) + assert result._cpt.attrs == {} + assert result._cpt.coords.dims == ("Node1", "Node2") + assert result._cpt.coords["Node1"].values.tolist() == ["Outcome1", "Outcome2"] + assert result._cpt.coords["Node2"].values.tolist() == ["Outcome21", "Outcome22"] + assert result._cpt.data.tolist() == [[0.2, 0.4], [0.1, 0.3]] + + +def test_class_ProbabilityConversion_to_json_conditional(): + values = np.array([[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]]) + conditioned_variable = {"A": ["yes", "no"]} + conditioning_variable = {"B": ["low", "mid", "high"]} + variables = conditioned_variable | conditioning_variable + cpt = DiscreteConditionalProbability(values, variables) + result = ProbabilityConversion().to_json(cpt) + + target = { + "dtype": "DiscreteConditionalProbability", + "probability_function": [[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]], + "variables": {"A": ["yes", "no"], "B": ["low", "mid", "high"]}, + } + assert result == target + + +def test_class_ProbabilityConversion_to_json_unconditional(): + values = np.array([[0.2, 0.4], [0.1, 0.3]]) + variables = {"A": ["yes", "no"], "B": ["low", "high"]} + cpt = DiscreteUnconditionalProbability(values, variables) + result = ProbabilityConversion().to_json(cpt) + + target = { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.2, 0.4], [0.1, 0.3]], + "variables": {"A": ["yes", "no"], "B": ["low", "high"]}, + } + assert result == target + + +def test_class_DiscreteConditionalProbabilityConversion_from_json_fail(caplog): + as_json = { + "dtype": "Junk", + "probability_function": [[0.4, 0.5], [0.6, 0.5]], + "variables": { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + "attributes": { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + }, + } + with pytest.raises(Exception) as exc_info: + DiscreteConditionalProbabilityConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create a DiscreteConditionalProbability: Junk" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create a DiscreteConditionalProbability: Junk" + ) + + +def test_class_DiscreteConditionalProbabilityConversion_from_json(): + as_json = { + "dtype": "DiscreteConditionalProbability", + "probability_function": [[0.4, 0.5], [0.6, 0.5]], + "variables": { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + "attributes": { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + }, + } + result = DiscreteConditionalProbabilityConversion().from_json(as_json) + assert isinstance(result, DiscreteConditionalProbability) + assert result._cpt.attrs == { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + } + assert result._cpt.coords.dims == ("Node1", "Node2") + assert result._cpt.coords["Node1"].values.tolist() == ["Outcome1", "Outcome2"] + assert result._cpt.coords["Node2"].values.tolist() == ["Outcome21", "Outcome22"] + assert result._cpt.data.tolist() == [[0.4, 0.5], [0.6, 0.5]] + + +def test_class_DiscreteConditionalProbabilityConversion_to_json(): + values = np.array([[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]]) + conditioned_variable = {"A": ["yes", "no"]} + conditioning_variable = {"B": ["low", "mid", "high"]} + variables = conditioned_variable | conditioning_variable + cpt = DiscreteConditionalProbability(values, variables) + result = DiscreteConditionalProbabilityConversion().to_json(cpt) + + target = { + "dtype": "DiscreteConditionalProbability", + "probability_function": [[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]], + "variables": {"A": ["yes", "no"], "B": ["low", "mid", "high"]}, + } + assert result == target + + +def test_class_DiscreteUnconditionalProbabilityConversion_from_json_fail(caplog): + as_json = { + "dtype": "Junk", + "probability_function": [[0.4, 0.5], [0.6, 0.5]], + "variables": { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + "attributes": { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + }, + } + with pytest.raises(Exception) as exc_info: + DiscreteUnconditionalProbabilityConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create a DiscreteUnconditionalProbability: Junk" + ] + assert ( + str(exc_info.value) + == "Data cannot be used to create a DiscreteUnconditionalProbability: Junk" + ) + + +def test_class_DiscreteUnconditionalProbabilityConversion_from_json(): + as_json = { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.2, 0.4], [0.1, 0.3]], + "variables": { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + "attributes": { + "conditioned_variables": ["Node1"], + "conditioning_variables": ["Node2"], + }, + } + result = DiscreteUnconditionalProbabilityConversion().from_json(as_json) + assert isinstance(result, DiscreteUnconditionalProbability) + assert result._cpt.attrs == {} + assert result._cpt.coords.dims == ("Node1", "Node2") + assert result._cpt.coords["Node1"].values.tolist() == ["Outcome1", "Outcome2"] + assert result._cpt.coords["Node2"].values.tolist() == ["Outcome21", "Outcome22"] + assert result._cpt.data.tolist() == [[0.2, 0.4], [0.1, 0.3]] + + +def test_class_DiscreteUnconditionalProbabilityConversion_to_json(): + values = np.array([[0.2, 0.4], [0.1, 0.3]]) + variables = {"A": ["yes", "no"], "B": ["low", "high"]} + cpt = DiscreteUnconditionalProbability(values, variables) + result = DiscreteUnconditionalProbabilityConversion().to_json(cpt) + + target = { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.2, 0.4], [0.1, 0.3]], + "variables": {"A": ["yes", "no"], "B": ["low", "high"]}, + } + assert result == target diff --git a/api/tests/v0/services/format_conversions/test_uncertainty_node.py b/api/tests/v0/services/format_conversions/test_uncertainty_node.py new file mode 100644 index 0000000..340595b --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_uncertainty_node.py @@ -0,0 +1,211 @@ +from copy import deepcopy + +import numpy as np +import pytest + +from src.v0.services.classes.discrete_conditional_probability import ( + DiscreteConditionalProbability, +) +from src.v0.services.classes.discrete_unconditional_probability import ( + DiscreteUnconditionalProbability, +) +from src.v0.services.classes.node import UncertaintyNode +from src.v0.services.format_conversions.node import UncertaintyNodeConversion + + +@pytest.fixture +def uncertainty_node(): + return { + "description": "testing node", + "shortname": "Node", + "boundary": "in", + "comments": [{"author": "Jr.", "comment": "Nope"}], + "category": "Uncertainty", + "keyUncertainty": "true", + "probabilities": { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.3], [0.7]], + "variables": {"States": ["s1", "s2"]}, + }, + "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + } + + +def test_class_UncertaintyNodeConversion_from_json_fail(caplog): + as_json = { + "category": "Junk", + "description": "C2H5OH", + "shortname": "veni vidi vici", + "probabilities": None, + } + with pytest.raises(Exception) as exc_info: + UncertaintyNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create a UncertaintyNode: Junk" + ] + assert str(exc_info.value) == "Data cannot be used to create a UncertaintyNode: Junk" + + +def test_UncertaintyNodeConversion_from_json_no_probability(uncertainty_node): + local_node = deepcopy(uncertainty_node) + local_node["probabilities"] = None + result = UncertaintyNodeConversion().from_json(local_node) + assert isinstance(result, UncertaintyNode) + assert result.description == "testing node" + assert result.shortname == "Node" + assert result.probability is None + + +def test_UncertaintyNodeConversion_from_json_unconditional(uncertainty_node): + result = UncertaintyNodeConversion().from_json(uncertainty_node) + assert isinstance(result, UncertaintyNode) + assert result.description == "testing node" + assert result.shortname == "Node" + assert result.probability.outcomes == (("s1", "s2")) + assert result.probability.variables == ("States",) + np.testing.assert_allclose( + result.probability.get_distribution(), np.array([0.3, 0.7]) + ) + + +def test_UncertaintyNodeConversion_from_json_conditional(uncertainty_node): + local_node = deepcopy(uncertainty_node) + local_node["probabilities"]["dtype"] = "DiscreteConditionalProbability" + local_node["probabilities"]["probability_function"] = [[0.3, 0.6], [0.7, 0.4]] + local_node["probabilities"]["variables"] = {"A": ["a1", "a2"], "B": ["b1", "b2"]} + result = UncertaintyNodeConversion().from_json(local_node) + assert isinstance(result, UncertaintyNode) + assert result.description == "testing node" + assert result.shortname == "Node" + assert result.probability.outcomes == ("a1", "a2") + assert result.probability.variables == ("A", "B") + np.testing.assert_allclose( + result.probability.get_distribution(), np.array([[0.3, 0.6], [0.7, 0.4]]) + ) + + +def test_UncertaintyNodeConversion_from_json_uncertainty_other(caplog, uncertainty_node): + local_node = deepcopy(uncertainty_node) + local_node["probabilities"]["dtype"] = "junk" + with pytest.raises(Exception) as exc_info: + UncertaintyNodeConversion().from_json(local_node) + assert [r.msg for r in caplog.records] == [ + ( + "Unreckonized probability type: " + "{'dtype': 'junk', " + "'probability_function': [[0.3], [0.7]], " + "'variables': {'States': ['s1', 's2']}}" + ), + ( + "Data cannot be used to create a UncertaintyNode: " + "" + "Unreckonized probability type: " + "{'dtype': 'junk', " + "'probability_function': [[0.3], [0.7]], " + "'variables': {'States': ['s1', 's2']}}" + ), + ] + assert str(exc_info.value) == ( + "Data cannot be used to create a UncertaintyNode: " + "" + "Unreckonized probability type: " + "{'dtype': 'junk', " + "'probability_function': [[0.3], [0.7]], " + "'variables': {'States': ['s1', 's2']}}" + ) + + +def test_UncertaintyNodeConversion_to_json_no_probability(uncertainty_node): + data = UncertaintyNode( + description=uncertainty_node["description"], + shortname=uncertainty_node["shortname"], + ) + result = UncertaintyNodeConversion().to_json(data) + assert result["description"] == uncertainty_node["description"] + assert result["shortname"] == uncertainty_node["shortname"] + assert result["keyUncertainty"] == "true" + assert result["uuid"] == data.uuid + assert result["probabilities"] is None + + +def test_UncertaintyNodeConversion_to_json_unconditional(uncertainty_node): + probabilities = DiscreteUnconditionalProbability( + probability_function=np.array([[0.25, 0.2], [0.25, 0.3]]), + variables={ + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + ) + data = UncertaintyNode( + description=uncertainty_node["description"], + shortname=uncertainty_node["shortname"], + probability=probabilities, + ) + result = UncertaintyNodeConversion().to_json(data) + assert result["description"] == uncertainty_node["description"] + assert result["shortname"] == uncertainty_node["shortname"] + assert result["keyUncertainty"] == "true" + assert result["uuid"] == data.uuid + assert result["probabilities"] == { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [[0.25, 0.2], [0.25, 0.3]], + "variables": { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + } + + +def test_UncertaintyNodeConversion_to_json_conditional(uncertainty_node): + probabilities = DiscreteConditionalProbability( + probability_function=np.array([[0.5, 0.4], [0.5, 0.6]]), + variables={ + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + ) + data = UncertaintyNode( + description=uncertainty_node["description"], + shortname=uncertainty_node["shortname"], + probability=probabilities, + ) + result = UncertaintyNodeConversion().to_json(data) + assert result["description"] == uncertainty_node["description"] + assert result["shortname"] == uncertainty_node["shortname"] + assert result["keyUncertainty"] == "true" + assert result["uuid"] == data.uuid + assert result["probabilities"]["dtype"] == "DiscreteConditionalProbability" + assert result["probabilities"]["variables"] == { + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + } + assert result["probabilities"]["probability_function"] == [[0.5, 0.4], [0.5, 0.6]] + + +def test_UncertaintyNodeConversion_to_json_uncertainty_other(caplog, uncertainty_node): + probabilities = DiscreteConditionalProbability( + probability_function=np.array([[0.5, 0.4], [0.5, 0.6]]), + variables={ + "Node1": ["Outcome1", "Outcome2"], + "Node2": ["Outcome21", "Outcome22"], + }, + ) + data = UncertaintyNode( + description=uncertainty_node["description"], + shortname=uncertainty_node["shortname"], + probability=probabilities, + ) + data._probability = "None" + with pytest.raises(Exception) as exc_info: + UncertaintyNodeConversion().to_json(data) + assert [r.msg for r in caplog.records] == [ + "Unreckonized probability type: None", + ( + "Data cannot be used to create a UncertaintyNode: " + "Unreckonized probability type: None" + ), + ] + assert str(exc_info.value) == ( + "Data cannot be used to create a UncertaintyNode: " + "Unreckonized probability type: None" + ) diff --git a/api/tests/v0/services/format_conversions/test_utility_node.py b/api/tests/v0/services/format_conversions/test_utility_node.py new file mode 100644 index 0000000..3a725d1 --- /dev/null +++ b/api/tests/v0/services/format_conversions/test_utility_node.py @@ -0,0 +1,47 @@ +import pytest + +from src.v0.services.classes.node import UtilityNode +from src.v0.services.format_conversions.node import UtilityNodeConversion + + +@pytest.fixture +def utility_node(): + return { + "description": "testing node", + "shortname": "Node", + "boundary": "in", + "comments": [{"author": "Jr.", "comment": "Nope"}], + "category": "Value Metric", + "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", + } + + +def test_class_UtilityNodeConversion_from_json_fail(caplog): + as_json = { + "category": "Junk", + "description": "C2H5OH", + "shortname": "veni vidi vici", + } + with pytest.raises(Exception) as exc_info: + UtilityNodeConversion().from_json(as_json) + assert [r.msg for r in caplog.records] == [ + "Data cannot be used to create a UtilityNode: Junk" + ] + assert str(exc_info.value) == "Data cannot be used to create a UtilityNode: Junk" + + +def test_UtilityNodeConversion_from_json(utility_node): + result = UtilityNodeConversion().from_json(utility_node) + assert isinstance(result, UtilityNode) + assert result.description == "testing node" + assert result.shortname == "Node" + + +def test_UtilityNodeConversion_to_json(utility_node): + data = UtilityNode( + description=utility_node["description"], shortname=utility_node["shortname"] + ) + result = UtilityNodeConversion().to_json(data) + assert result["description"] == utility_node["description"] + assert result["shortname"] == utility_node["shortname"] + assert result["uuid"] == data.uuid diff --git a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_decision_tree.py b/api/tests/v0/services/structure_utils/test_decision_diagrams/test_decision_tree.py deleted file mode 100644 index cdd2c51..0000000 --- a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_decision_tree.py +++ /dev/null @@ -1,202 +0,0 @@ -import json -from unittest.mock import mock_open, patch - -import networkx as nx -import numpy as np -import pytest - -from src.v0.services.structure_utils.decision_diagrams.decision_tree import DecisionTree -from src.v0.services.structure_utils.decision_diagrams.edge import Edge -from src.v0.services.structure_utils.decision_diagrams.node import ( - DecisionNode, - UncertaintyNode, - UtilityNode, -) - - -@pytest.fixture -def graph_as_dict(): - n0 = UncertaintyNode("u0", "Uncertainty node 0") - n1 = UncertaintyNode("u1", "Uncertainty node 1") - n2 = UncertaintyNode("u2", "Uncertainty node 2") - n3 = DecisionNode("d0", "Decision node 0") - n4 = DecisionNode("d1", "Decision node 1") - n5 = DecisionNode("d2", "Decision node 2") - n6 = DecisionNode("d3", "Decision node 3") - n7 = DecisionNode("d4", "Decision node 4") - n8 = UtilityNode("v0", "Utility node 0") - n9 = UtilityNode("v1", "Utility node 1") - n10 = UtilityNode("v2", "Utility node 2") - n11 = UtilityNode("v3", "Utility node 3") - n12 = UncertaintyNode("u3", "Uncertainty node 3") - n13 = UtilityNode("v4", "Utility node 4") - n14 = UtilityNode("v5", "Utility node 5") - n15 = UtilityNode("v6", "Utility node 6") - n16 = UtilityNode("v7", "Utility node 7") - n17 = UtilityNode("v8", "Utility node 8") - n18 = UtilityNode("v9", "Utility node 9") - n19 = UtilityNode("v10", "Utility node 10") - n20 = UtilityNode("v11", "Utility node 11") - n21 = UtilityNode("v12", "Utility node 12") - - e0 = Edge(n0, n1, name="e0") - e1 = Edge(n1, n3, name="e1") - e2 = Edge(n3, n8, name="e2") - e3 = Edge(n3, n9, name="e3") - e4 = Edge(n1, n4, name="e4") - e5 = Edge(n4, n10, name="e5") - e6 = Edge(n4, n11, name="e6") - e7 = Edge(n4, n12, name="e7") - e8 = Edge(n12, n20, name="e8") - e9 = Edge(n12, n21, name="e9") - e10 = Edge(n0, n2, name="e10") - e11 = Edge(n2, n5, name="e11") - e12 = Edge(n5, n13, name="e12") - e13 = Edge(n5, n14, name="e13") - e14 = Edge(n2, n6, name="e14") - e15 = Edge(n6, n15, name="e15") - e16 = Edge(n6, n16, name="e16") - e17 = Edge(n2, n7, name="e17") - e18 = Edge(n7, n17, name="e18") - e19 = Edge(n7, n18, name="e19") - e20 = Edge(n7, n19, name="e20") - - return { - "nodes": [ - n0, - n1, - n2, - n3, - n4, - n5, - n6, - n7, - n8, - n9, - n10, - n11, - n12, - n13, - n14, - n15, - n16, - n17, - n18, - n19, - n20, - n21, - ], - "edges": [ - e0, - e1, - e2, - e3, - e4, - e5, - e6, - e7, - e8, - e9, - e10, - e11, - e12, - e13, - e14, - e15, - e16, - e17, - e18, - e19, - e20, - ], - } - - -def test_from_dict(graph_as_dict): - dt = DecisionTree.from_dict(graph_as_dict) - assert isinstance(dt.nx, nx.DiGraph) - - -def test_class_DecisionTree(): - dt = DecisionTree() - assert isinstance(dt.nx, nx.DiGraph) - - -def test_set_root(graph_as_dict): - dt = DecisionTree() - root = graph_as_dict["nodes"][0] - dt.set_root(root) - assert isinstance(dt.nx, nx.DiGraph) - assert dt.parent(root) is None - assert len(dt.get_children(root)) == 0 - - -def test_initialize_decision_tree(graph_as_dict): - n0 = graph_as_dict["nodes"][0] - dt = DecisionTree(root=n0) - assert nx.number_of_nodes(dt.nx) == 1 - - -def test_initialize_decision_tree_fail(): - n0 = UncertaintyNode("u0", "Uncertainty node 0") - n1 = UncertaintyNode("u1", "Uncertainty node 1") - n2 = UncertaintyNode("u2", "Uncertainty node 2") - n3 = DecisionNode("d0", "Decision node 0") - n4 = DecisionNode("d1", "Decision node 1") - n5 = DecisionNode("d2", "Decision node 2") - - e0 = Edge(n0, n5, name="e0") - e1 = Edge(n1, n2, name="e1") - e2 = Edge(n2, n3, name="e2") - e3 = Edge(n2, n4, name="e3") - - graph = { - "nodes": [n0, n1, n2, n3, n4, n5], - "edges": [e0, e1, e2, e3], - } - - with pytest.raises(Exception) as exc: - DecisionTree.initialize_diagram(graph) - assert str(exc.value) == "Decision tree has no defined root node" - - -def test_parent(graph_as_dict): - dt = DecisionTree.from_dict(graph_as_dict) - n5 = graph_as_dict["nodes"][5] - n2 = graph_as_dict["nodes"][2] - assert dt.parent(n5) == n2 - - -def test_to_json(graph_as_dict): - dt = DecisionTree.from_dict(graph_as_dict) - result = dt.to_json() - assert isinstance(json.loads(result), dict) - assert result.count("description") == 22 - assert result.count("shortname") == 22 - assert result.count("probabilities") == 4 - assert result.count("alternatives") == 5 - assert result.count("utility") == 13 - assert result.count("children") == 9 - - -def test_to_json_with_file(graph_as_dict): - dt = DecisionTree.from_dict(graph_as_dict) - with patch("builtins.open", mock_open()) as m: - dt.to_json("junk") - m.assert_called_once_with("junk", "w") - - -def test_to_json_fail_no_root(caplog): - probabilities = { - "type": "DiscreteUnconditionalProbability", - "probability_function": np.array([0.8, 0.2]), - "variables": {"State": ["Peach", "Lemon"]}, - } - uncertainty = UncertaintyNode("Symptom", "S", probabilities=probabilities) - - dt = DecisionTree() - dt.add_node(uncertainty) - with pytest.raises(Exception) as exc_info: - dt.to_json() - assert [r.msg for r in caplog.records] == ["Decision tree has no defined root node"] - assert str(exc_info.value) == "Decision tree has no defined root node" diff --git a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_edge.py b/api/tests/v0/services/structure_utils/test_decision_diagrams/test_edge.py deleted file mode 100644 index 5158adf..0000000 --- a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_edge.py +++ /dev/null @@ -1,123 +0,0 @@ -import pytest - -from src.v0.services.structure_utils.decision_diagrams.edge import Edge -from src.v0.services.structure_utils.decision_diagrams.influence_diagram import ( - InfluenceDiagram, -) -from src.v0.services.structure_utils.decision_diagrams.node import ( - DecisionNode, - UncertaintyNode, - UtilityNode, -) - - -def test_class_InformationalArc(): - n1 = UncertaintyNode("J", "junk") - n2 = DecisionNode("H", "junky") - edge = Edge(n1, n2, "first") - assert edge.endpoint_start == n1 - assert edge.endpoint_end == n2 - assert edge.name == "first" - assert edge._arc_type == "informational" - - -def test_class_InformationalArc_without_name(): - n1 = UncertaintyNode("J", "junk") - n2 = DecisionNode("H", "junky") - edge = Edge(n1, n2) - assert edge.name is None - - -def test_class_ConditionalArc(): - n1 = UncertaintyNode("J", "junk") - n2 = UncertaintyNode("H", "junky") - edge = Edge(n1, n2, "second") - assert edge.endpoint_start.description == "junk" - assert edge.endpoint_start.shortname == "J" - assert edge.endpoint_end.description == "junky" - assert edge.endpoint_end.shortname == "H" - assert edge._name == "second" - assert edge._arc_type == "conditional" - - -def test_class_FunctionalArc(): - n1 = UncertaintyNode("J", "junk") - n2 = UtilityNode("H", "junky") - edge = Edge(n1, n2, "first") - assert edge.endpoint_start == n1 - assert edge.endpoint_end == n2 - assert edge.name == "first" - assert edge._arc_type == "functional" - - -def test_copy(): - n1 = UncertaintyNode("J", "junk") - n2 = DecisionNode("H", "junky") - edge = Edge(n1, n2, "first") - copied_edge = edge.copy() - assert copied_edge.endpoint_start == edge.endpoint_start - assert copied_edge.endpoint_end == edge.endpoint_end - assert copied_edge.name == edge.name - - -def test_set_endpoint_with_mode_end(): - n1 = UncertaintyNode("J", "junk") - n2 = DecisionNode("H", "junky") - edge = Edge(n1, None, "first") - assert edge.endpoint_start == n1 - assert edge.endpoint_end is None - assert edge._name == "first" - assert edge._arc_type is None - edge.set_endpoint(n2) - assert edge.endpoint_end == n2 - assert edge._arc_type == "informational" - - -def test_set_endpoint_with_mode_end_failing_for_wrong_mode(caplog): - n1 = UncertaintyNode("J", "junk") - n2 = DecisionNode("H", "junky") - edge = Edge(n1, None, "first") - with pytest.raises(Exception) as exc_info: - edge.set_endpoint(n2, mode="junk") - assert [r.msg for r in caplog.records] == ["endpoint cannot be set to mode junk"] - assert str(exc_info.value) == "endpoint cannot be set to mode junk" - - -def test_set_endpoint_with_mode_end_failing_for_wrong_end_node(caplog): - n1 = UtilityNode("J", "junk") - n2 = DecisionNode("H", "junky") - edge = Edge(n1, None, "first") - with pytest.raises(Exception) as exc_info: - edge.set_endpoint(n2, mode="end") - assert [r.msg for r in caplog.records] == [ - "utility node can only have other utility nodes as successor" - ] - assert ( - str(exc_info.value) - == "utility node can only have other utility nodes as successor" - ) - - -def test_set_endpoint_with_mode_start(): - n1 = UncertaintyNode("J", "junk") - n2 = DecisionNode("H", "junky") - edge = Edge(None, n2, "first") - assert edge.endpoint_start is None - assert edge.endpoint_end == n2 - assert edge._name == "first" - assert edge._arc_type == "informational" - edge.set_endpoint(n1, mode="start") - assert edge.endpoint_start == n1 - assert edge._arc_type == "informational" - - -def test_from_dict(): - n1 = UncertaintyNode("J", "junk") - n2 = DecisionNode("H", "junky") - ID = InfluenceDiagram.from_dict({"nodes": [n1, n2]}) - edge = {"from": n1.uuid, "to": n2.uuid, "name": "first"} - edge = Edge.from_dict(edge, ID) - assert edge.endpoint_start == n1 - assert edge.endpoint_end == n2 - assert edge.name == "first" - assert edge._arc_type == "informational" diff --git a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_influence_diagram.py b/api/tests/v0/services/structure_utils/test_decision_diagrams/test_influence_diagram.py deleted file mode 100644 index 67765b3..0000000 --- a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_influence_diagram.py +++ /dev/null @@ -1,1067 +0,0 @@ -import json - -import networkx as nx -import pyAgrum as gum -import pytest - -from src.v0.models.issue import ProbabilityData -from src.v0.models.structure import InfluenceDiagramResponse -from src.v0.services.structure_utils.decision_diagrams.decision_tree import DecisionTree -from src.v0.services.structure_utils.decision_diagrams.edge import Edge -from src.v0.services.structure_utils.decision_diagrams.influence_diagram import ( - InfluenceDiagram, -) -from src.v0.services.structure_utils.decision_diagrams.node import ( - DecisionNode, - UncertaintyNode, - UtilityNode, -) -from src.v0.services.structure_utils.probability.discrete_unconditional_probability import ( # noqa: E501 - DiscreteUnconditionalProbability, -) - -TESTDATA = "v0/services/testdata" - - -@pytest.fixture -def graph_as_dict(): - n0 = UncertaintyNode("u1", "Uncertainty node 1") - n1 = UncertaintyNode("u2", "Uncertainty node 2") - n2 = UncertaintyNode("u3", "Uncertainty node 3") - n3 = UncertaintyNode("u4", "Uncertainty node 4") - n4 = DecisionNode("d1", "Decision node 1") - n5 = UncertaintyNode("u5", "Uncertainty node 5") - n6 = DecisionNode("d2", "Decision node 2") - n7 = UncertaintyNode("u6", "Uncertainty node 6") - n8 = UncertaintyNode("u7", "Uncertainty node 7") - n9 = UncertaintyNode("u8", "Uncertainty node 8") - n10 = UtilityNode("v1", "Utility node 1") - - e0 = Edge(n0, n4, name="e0") - e1 = Edge(n1, n4, name="e1") - e2 = Edge(n2, n4, name="e2") - e3 = Edge(n3, n6, name="e3") - e4 = Edge(n4, n6, name="e4") - e5 = Edge(n4, n5, name="e5") - e6 = Edge(n6, n7, name="e6") - e7 = Edge(n6, n8, name="e7") - e8 = Edge(n6, n9, name="e8") - e9 = Edge(n5, n10, name="e9") - - return { - "nodes": [n0, n1, n2, n3, n4, n5, n6, n7, n8, n9, n10], - "edges": [e0, e1, e2, e3, e4, e5, e6, e7, e8, e9], - } - - -def test_from_dict(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - assert isinstance(ID.nx, nx.DiGraph) - assert ID.decision_count == 2 - assert ID.uncertainty_count == 8 - assert ID.utility_count == 1 - - -def test_from_db(copy_testdata_tmpdir, tmp_path): - copy_testdata_tmpdir(TESTDATA) - with open(tmp_path / "used_car_buyer_model_response.json") as f: - json_stream = json.load(f) - influence_diagram_response = InfluenceDiagramResponse( - vertices=json_stream["vertices"], edges=json_stream["edges"] - ) - InfluenceDiagram.from_db(influence_diagram_response) - - n0 = UncertaintyNode("State", "Joe does not know the state of the car") - n1 = UncertaintyNode("Test Result", "The result of the test is currently unknown") - n2 = DecisionNode("Buy", "We can buy the car") - n3 = DecisionNode("Test", "Joe can test the car") - n4 = UtilityNode("Value", "Value") - - e0 = Edge(n2, n4, name="e0") - e1 = Edge(n0, n1, name="e1") - e2 = Edge(n0, n4, name="e2") - e3 = Edge(n1, n2, name="e3") - e4 = Edge(n3, n1, name="e4") - - target = InfluenceDiagram.from_dict( - {"nodes": [n0, n1, n2, n3, n4], "edges": [e0, e1, e2, e3, e4]} - ) - result = InfluenceDiagram.from_db(influence_diagram_response) - - for result_node, target_node in zip(result.nx.nodes, target.nx.nodes, strict=False): - assert type(result_node) is type(target_node) - assert result_node.description == target_node.description - if not isinstance(result_node, UtilityNode): - assert result_node.shortname == target_node.shortname - - for result_arc, target_arc in zip(result.nx.edges, target.nx.edges, strict=False): - assert type(result_arc) is type(target_arc) - assert result_arc[0].description == target_arc[0].description - assert result_arc[1].description == target_arc[1].description - assert ( - result.nx.edges[result_arc[0], result_arc[1]]["arc_type"] - == target.nx.edges[target_arc[0], target_arc[1]]["arc_type"] - ) - - -def test_to_json(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - result = ID.to_json() - json_result = json.loads(result) - assert json_result["directed"] - assert not json_result["multigraph"] - assert len(json_result["nodes"]) == 11 - assert len(json_result["edges"]) == 10 - assert result.count("description") == 11 - assert result.count("shortname") == 11 - assert result.count("from") == 10 - assert result.count("to") == 10 - - -def test_class_InfluenceDiagram(): - ID = InfluenceDiagram() - assert isinstance(ID.nx, nx.DiGraph) - - -def test_copy(): - ID = InfluenceDiagram() - assert nx.utils.graphs_equal(ID.nx, ID.copy().nx) - - -def test_get_parents(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - n4 = graph_as_dict["nodes"][4] - result = ID.get_parents(n4) - target = graph_as_dict["nodes"][0:3] - assert result == target - - -def test_get_children(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - n6 = graph_as_dict["nodes"][6] - result = ID.get_children(n6) - target = graph_as_dict["nodes"][7:10] - assert result == target - - n4 = graph_as_dict["nodes"][4] - result = ID.get_children(n4) - target = [graph_as_dict["nodes"][5], graph_as_dict["nodes"][6]] - assert all(item in target for item in result) - assert all(item in result for item in target) - - -def test_get_node_type(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - n6 = graph_as_dict["nodes"][6] - assert ID.get_node_type(n6) == "decision" - - -def test_get_nodes_from_type(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - assert ID._get_nodes_from_type("DecisionNode") == [ - graph_as_dict["nodes"][4], - graph_as_dict["nodes"][6], - ] - assert ID._get_nodes_from_type("UtilityNode") == [graph_as_dict["nodes"][10]] - assert ID._get_nodes_from_type("UncertaintyNode") == [ - graph_as_dict["nodes"][0], - graph_as_dict["nodes"][1], - graph_as_dict["nodes"][2], - graph_as_dict["nodes"][3], - graph_as_dict["nodes"][5], - graph_as_dict["nodes"][7], - graph_as_dict["nodes"][8], - graph_as_dict["nodes"][9], - ] - - -def test_get_decision_nodes(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - assert ID.get_decision_nodes() == [ - graph_as_dict["nodes"][4], - graph_as_dict["nodes"][6], - ] - - -def test_get_utility_nodes(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - assert ID.get_utility_nodes() == [graph_as_dict["nodes"][10]] - - -def test_get_uncertainty_nodes(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - assert ID.get_uncertainty_nodes() == [ - graph_as_dict["nodes"][0], - graph_as_dict["nodes"][1], - graph_as_dict["nodes"][2], - graph_as_dict["nodes"][3], - graph_as_dict["nodes"][5], - graph_as_dict["nodes"][7], - graph_as_dict["nodes"][8], - graph_as_dict["nodes"][9], - ] - - -def test_nodes_count(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - assert ID.decision_count == 2 - assert ID.utility_count == 1 - assert ID.uncertainty_count == 8 - - -def test_has_children(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - n4 = graph_as_dict["nodes"][4] - n8 = graph_as_dict["nodes"][8] - assert ID.has_children(n4) - assert not ID.has_children(n8) - - -def test_get_node_from_uuid(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - n4 = graph_as_dict["nodes"][4] - uuid = n4.uuid - node = ID.get_node_from_uuid(uuid) - assert isinstance(node, DecisionNode) - assert node.description == "Decision node 1" - assert node.shortname == "d1" - - -def test_decision_elimination_order(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - result = ID.decision_elimination_order() - target = [graph_as_dict["nodes"][4], graph_as_dict["nodes"][6]] - assert all(item in target for item in result) - assert all(item in result for item in target) - - -def test_calculate_partial_order(graph_as_dict): - # Test is only reproducing result, not testing logic! - ID = InfluenceDiagram.from_dict(graph_as_dict) - partial_order = ID.calculate_partial_order() - result = [n.shortname for n in partial_order] - target = ["u1", "u2", "u3", "d1", "u4", "d2", "u5", "u6", "u7", "u8"] - assert result == target - - -def test_calculate_partial_order_fail_mode(graph_as_dict, caplog): - # Test is only reproducing result, not testing logic! - ID = InfluenceDiagram.from_dict(graph_as_dict) - with pytest.raises(Exception) as exc_info: - ID.calculate_partial_order(mode="junk") - assert [r.msg for r in caplog.records] == [ - "output mode should be [view|copy] and have been entered as junk" - ] - assert ( - str(exc_info.value) - == "output mode should be [view|copy] and have been entered as junk" - ) - - -def test_calculate_partial_order_copy_mode(graph_as_dict): - # Test is only reproducing result, not testing logic! - ID = InfluenceDiagram.from_dict(graph_as_dict) - partial_order_0 = ID.calculate_partial_order(mode="view") - partial_order = ID.calculate_partial_order(mode="copy") - result = [n.shortname for n in partial_order] - target = ["u1", "u2", "u3", "d1", "u4", "d2", "u5", "u6", "u7", "u8"] - assert result == target - assert partial_order_0 != partial_order - - -def test_output_branches_from_node_empty_lists(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - uncertainty_node = graph_as_dict["nodes"][0] - decision_node = graph_as_dict["nodes"][4] - utility_node = graph_as_dict["nodes"][10] - - assert ( - len( - list( - zip( - *ID._output_branches_from_node(uncertainty_node, uncertainty_node), - strict=False, - ) - ) - ) - == 0 - ) - assert ( - len( - list( - zip( - *ID._output_branches_from_node(decision_node, uncertainty_node), - strict=False, - ) - ) - ) - == 0 - ) - assert ( - len( - list( - zip( - *ID._output_branches_from_node(utility_node, uncertainty_node), - strict=False, - ) - ) - ) - == 0 - ) - - -def test_output_branches_from_node(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - probability = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.7, 0.2, 0.1]], - "variables": {"State": ["pear", "lemon", "plum"]}, - } - ) - uncertainty_node = graph_as_dict["nodes"][0] - uncertainty_node._probabilities = DiscreteUnconditionalProbability.from_db_model( - probability - ) - decision_node = graph_as_dict["nodes"][4] - decision_node._alternatives = ["wait", "pickup"] - utility_node = graph_as_dict["nodes"][10] - utility_node._utility = ["1000"] - - result = list(ID._output_branches_from_node(uncertainty_node, uncertainty_node)) - assert all(r[1] == uncertainty_node for r in result) - assert [r[0].name for r in result] == ["plum", "lemon", "pear"] - assert all(isinstance(r[0], Edge) for r in result) - assert all(r[0].endpoint_start == uncertainty_node for r in result) - assert all(r[0].endpoint_end is None for r in result) - - result = list(ID._output_branches_from_node(decision_node, uncertainty_node)) - assert all(r[1] == uncertainty_node for r in result) - assert [r[0].name for r in result] == ["pickup", "wait"] - assert all(isinstance(r[0], Edge) for r in result) - assert all(r[0].endpoint_start == decision_node for r in result) - assert all(r[0].endpoint_end is None for r in result) - - result = list(ID._output_branches_from_node(utility_node, uncertainty_node)) - assert all(r[1] == uncertainty_node for r in result) - assert [r[0].name for r in result] == ["1000"] - assert all(isinstance(r[0], Edge) for r in result) - assert all(r[0].endpoint_start == utility_node for r in result) - assert all(r[0].endpoint_end is None for r in result) - - -def test_output_branches_from_node_reverse_mode(graph_as_dict): - ID = InfluenceDiagram.from_dict(graph_as_dict) - probability = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.7, 0.2, 0.1]], - "variables": {"State": ["pear", "lemon", "plum"]}, - } - ) - uncertainty_node = graph_as_dict["nodes"][0] - uncertainty_node._probabilities = DiscreteUnconditionalProbability.from_db_model( - probability - ) - - result = list( - ID._output_branches_from_node(uncertainty_node, uncertainty_node, flip=False) - ) - assert [r[0].name for r in result] == ["pear", "lemon", "plum"] - - -def test_convert_to_decision_tree_symmetry(): - # Medical Diagnosis Problem - # Data taken from - # DECISION TREES AND INFLUENCE DIAGRAMS - # Prakash P. Shenoy - # Encyclopedia of Life Support Systems, U Derigs (ed.), - # Optimization and Operations Research, Vol. 4, pp. 280–298, 2009 - # - # - # Probabilities in the ID to be updated!!! - # - # The produced tree may be flipped horizontally compared to the expected one - - probability_S = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.7, - 0.3, - ] - ], - "variables": { - "State": [ - "yes", - "no", - ] - }, - } - ) - probability_S = DiscreteUnconditionalProbability.from_db_model(probability_S) - probability_P = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.2, - 0.8, - ] - ], - "variables": { - "State": [ - "yes", - "no", - ] - }, - } - ) - probability_P = DiscreteUnconditionalProbability.from_db_model(probability_P) - probability_D = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.1, - 0.9, - ] - ], - "variables": { - "State": [ - "yes", - "no", - ] - }, - } - ) - probability_D = DiscreteUnconditionalProbability.from_db_model(probability_D) - - id_decision_T = DecisionNode("T", "Treat for Disease", alternatives=["yes", "no"]) - id_uncertainty_S = UncertaintyNode("S", "Symptom", probabilities=probability_S) - id_uncertainty_P = UncertaintyNode( - "P", "Pathological state", probabilities=probability_P - ) - id_uncertainty_D = UncertaintyNode("D", "Disease", probabilities=probability_D) - id_utility_0 = UtilityNode("v", "Utility", utility=None) - - id_e0 = Edge(id_uncertainty_S, id_decision_T) - id_e1 = Edge(id_uncertainty_P, id_uncertainty_S) - id_e2 = Edge(id_uncertainty_D, id_uncertainty_P) - id_e3 = Edge(id_decision_T, id_utility_0) - id_e4 = Edge(id_uncertainty_P, id_utility_0) - id_e5 = Edge(id_uncertainty_D, id_utility_0) - - net = { - "nodes": [ - id_decision_T, - id_uncertainty_S, - id_uncertainty_P, - id_uncertainty_D, - id_utility_0, - ], - "edges": [id_e0, id_e1, id_e2, id_e3, id_e4, id_e5], - } - - ID = InfluenceDiagram() - for node in net["nodes"]: - ID.add_node(node) - for edge in net["edges"]: - ID.add_edge(edge) - - dt_uncertainty_S = UncertaintyNode("S", "Symptom", probabilities=probability_S) - dt_decision_T_0 = DecisionNode("T", "Treat for Disease", alternatives=["yes", "no"]) - dt_e0 = Edge(dt_uncertainty_S, dt_decision_T_0, name="yes") - dt_uncertainty_P_0 = UncertaintyNode( - "P", "Pathological state", probabilities=probability_P - ) - dt_e1 = Edge(dt_decision_T_0, dt_uncertainty_P_0, name="yes") - dt_uncertainty_D_0 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e2 = Edge(dt_uncertainty_P_0, dt_uncertainty_D_0, name="yes") - dt_utility_0 = UtilityNode("v", "Utility", utility=None) - dt_e3 = Edge(dt_uncertainty_D_0, dt_utility_0, name="yes") - dt_utility_1 = UtilityNode("v", "Utility", utility=None) - dt_e4 = Edge(dt_uncertainty_D_0, dt_utility_1, name="no") - dt_uncertainty_D_1 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e5 = Edge(dt_uncertainty_P_0, dt_uncertainty_D_1, name="no") - dt_utility_2 = UtilityNode("v", "Utility", utility=None) - dt_e6 = Edge(dt_uncertainty_D_1, dt_utility_2, name="yes") - dt_utility_3 = UtilityNode("v", "Utility", utility=None) - dt_e7 = Edge(dt_uncertainty_D_1, dt_utility_3, name="no") - dt_uncertainty_P_1 = UncertaintyNode( - "P", "Pathological state", probabilities=probability_P - ) - dt_e8 = Edge(dt_decision_T_0, dt_uncertainty_P_1, name="no") - dt_uncertainty_D_2 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e9 = Edge(dt_uncertainty_P_1, dt_uncertainty_D_2, name="yes") - dt_utility_4 = UtilityNode("v", "Utility", utility=None) - dt_e10 = Edge(dt_uncertainty_D_2, dt_utility_4, name="yes") - dt_utility_5 = UtilityNode("v", "Utility", utility=None) - dt_e11 = Edge(dt_uncertainty_D_2, dt_utility_5, name="no") - dt_uncertainty_D_3 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e12 = Edge(dt_uncertainty_P_1, dt_uncertainty_D_3, name="no") - dt_utility_6 = UtilityNode("v", "Utility", utility=None) - dt_e13 = Edge(dt_uncertainty_D_3, dt_utility_6, name="yes") - dt_utility_7 = UtilityNode("v", "Utility", utility=None) - dt_e14 = Edge(dt_uncertainty_D_3, dt_utility_7, name="no") - dt_decision_T_1 = DecisionNode("T", "Treat for Disease", alternatives=["yes", "no"]) - dt_e15 = Edge(dt_uncertainty_S, dt_decision_T_1, name="no") - dt_uncertainty_P_2 = UncertaintyNode( - "P", "Pathological state", probabilities=probability_P - ) - dt_e16 = Edge(dt_decision_T_1, dt_uncertainty_P_2, name="yes") - dt_uncertainty_D_4 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e17 = Edge(dt_uncertainty_P_2, dt_uncertainty_D_4, name="yes") - dt_utility_8 = UtilityNode("v", "Utility", utility=None) - dt_e18 = Edge(dt_uncertainty_D_4, dt_utility_8, name="yes") - dt_utility_9 = UtilityNode("v", "Utility", utility=None) - dt_e19 = Edge(dt_uncertainty_D_4, dt_utility_9, name="no") - dt_uncertainty_D_5 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e20 = Edge(dt_uncertainty_P_2, dt_uncertainty_D_5, name="no") - dt_utility_10 = UtilityNode("v", "Utility", utility=None) - dt_e21 = Edge(dt_uncertainty_D_5, dt_utility_10, name="yes") - dt_utility_11 = UtilityNode("v", "Utility", utility=None) - dt_e22 = Edge(dt_uncertainty_D_5, dt_utility_11, name="no") - dt_uncertainty_P_3 = UncertaintyNode( - "P", "Pathological state", probabilities=probability_P - ) - dt_e23 = Edge(dt_decision_T_1, dt_uncertainty_P_3, name="no") - dt_uncertainty_D_6 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e24 = Edge(dt_uncertainty_P_3, dt_uncertainty_D_6, name="yes") - dt_utility_12 = UtilityNode("v", "Utility", utility=None) - dt_e25 = Edge(dt_uncertainty_D_6, dt_utility_12, name="yes") - dt_utility_13 = UtilityNode("v", "Utility", utility=None) - dt_e26 = Edge(dt_uncertainty_D_6, dt_utility_13, name="no") - dt_uncertainty_D_7 = UncertaintyNode("D", "Disease", probabilities=probability_D) - dt_e27 = Edge(dt_uncertainty_P_3, dt_uncertainty_D_7, name="no") - dt_utility_14 = UtilityNode("v", "Utility", utility=None) - dt_e28 = Edge(dt_uncertainty_D_7, dt_utility_14, name="yes") - dt_utility_15 = UtilityNode("v", "Utility", utility=None) - dt_e29 = Edge(dt_uncertainty_D_7, dt_utility_15, name="no") - - nodes = [ - dt_uncertainty_S, - dt_decision_T_0, - dt_uncertainty_P_0, - dt_uncertainty_D_0, - dt_utility_0, - dt_utility_1, - dt_uncertainty_D_1, - dt_utility_2, - dt_utility_3, - dt_uncertainty_P_1, - dt_uncertainty_D_2, - dt_utility_4, - dt_utility_5, - dt_uncertainty_D_3, - dt_utility_6, - dt_utility_7, - dt_decision_T_1, - dt_uncertainty_P_2, - dt_uncertainty_D_4, - dt_utility_8, - dt_utility_9, - dt_uncertainty_D_5, - dt_utility_10, - dt_utility_11, - dt_uncertainty_P_3, - dt_uncertainty_D_6, - dt_utility_12, - dt_utility_13, - dt_uncertainty_D_7, - dt_utility_14, - dt_utility_15, - ] - net = { - "nodes": nodes, - "edges": [ - dt_e0, - dt_e1, - dt_e2, - dt_e3, - dt_e4, - dt_e5, - dt_e6, - dt_e7, - dt_e8, - dt_e9, - dt_e10, - dt_e11, - dt_e12, - dt_e13, - dt_e14, - dt_e15, - dt_e16, - dt_e17, - dt_e18, - dt_e19, - dt_e20, - dt_e21, - dt_e22, - dt_e23, - dt_e24, - dt_e25, - dt_e26, - dt_e27, - dt_e28, - dt_e29, - ], - } - - DT = DecisionTree(root=dt_uncertainty_S) - for node in net["nodes"]: - DT.add_node(node) - for edge in net["edges"]: - DT.add_edge(edge) - - IDT = ID.convert_to_decision_tree() - for id_node, dt_node in zip(IDT.nx.nodes, DT.nx.nodes, strict=False): - assert type(id_node) is type(dt_node) - assert id_node.description == dt_node.description - if not isinstance(id_node, UtilityNode): - assert id_node.shortname == dt_node.shortname - - for id_edge, dt_edge in zip(IDT.nx.edges, DT.nx.edges, strict=False): - assert type(id_edge) is type(dt_edge) - assert id_edge[0].description == dt_edge[0].description - assert id_edge[1].description == dt_edge[1].description - assert ( - IDT.nx.edges[id_edge[0], id_edge[1]]["name"] - == DT.nx.edges[dt_edge[0], dt_edge[1]]["name"] - ) - assert ( - IDT.nx.edges[id_edge[0], id_edge[1]]["arc_type"] - == DT.nx.edges[dt_edge[0], dt_edge[1]]["arc_type"] - ) - - -def test_convert_to_decision_tree_simple_order_asymmetry(): - probability_U1 = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.7, - 0.3, - ] - ], - "variables": { - "State": [ - "high", - "low", - ] - }, - } - ) - probability_U1 = DiscreteUnconditionalProbability.from_db_model(probability_U1) - probability_U2 = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.2, - 0.8, - ] - ], - "variables": { - "State": [ - "yes", - "no", - ] - }, - } - ) - probability_U2 = DiscreteUnconditionalProbability.from_db_model(probability_U2) - - id_uncertainty_u1 = UncertaintyNode("u1", "U1", probabilities=probability_U1) - id_uncertainty_u2 = UncertaintyNode("u2", "U2", probabilities=probability_U2) - id_decision = DecisionNode("D", "D", alternatives=["green", "red"]) - - id_e0 = Edge(id_uncertainty_u1, id_decision) - id_e1 = Edge(id_uncertainty_u2, id_decision) - - net = { - "nodes": [id_uncertainty_u1, id_uncertainty_u2, id_decision], - "edges": [id_e0, id_e1], - } - - ID = InfluenceDiagram() - for node in net["nodes"]: - ID.add_node(node) - for edge in net["edges"]: - ID.add_edge(edge) - - dt_uncertainty_u1 = UncertaintyNode("u1", "U1", probabilities=probability_U1) - - dt_uncertainty_u2_1 = UncertaintyNode("u2", "U2", probabilities=probability_U2) - dt_e0 = Edge(dt_uncertainty_u1, dt_uncertainty_u2_1, name="high") - dt_decision_1 = DecisionNode("D", "D", alternatives=["green", "red"]) - dt_e1 = Edge(dt_uncertainty_u2_1, dt_decision_1, name="yes") - dt_utility_0 = UtilityNode("v", "Utility", utility=None) - dt_e2 = Edge(dt_decision_1, dt_utility_0, name="green") - dt_utility_1 = UtilityNode("v", "Utility", utility=None) - dt_e3 = Edge(dt_decision_1, dt_utility_1, name="red") - - dt_decision_2 = DecisionNode("D", "D", alternatives=["green", "red"]) - dt_e4 = Edge(dt_uncertainty_u2_1, dt_decision_2, name="no") - dt_utility_2 = UtilityNode("v", "Utility", utility=None) - dt_e5 = Edge(dt_decision_2, dt_utility_2, name="green") - dt_utility_3 = UtilityNode("v", "Utility", utility=None) - dt_e6 = Edge(dt_decision_2, dt_utility_3, name="red") - - dt_uncertainty_u2_2 = UncertaintyNode("u2", "U2", probabilities=probability_U2) - dt_e7 = Edge(dt_uncertainty_u1, dt_uncertainty_u2_2, name="low") - dt_decision_3 = DecisionNode("D", "D", alternatives=["green", "red"]) - dt_e8 = Edge(dt_uncertainty_u2_2, dt_decision_3, name="yes") - dt_utility_4 = UtilityNode("v", "Utility", utility=None) - dt_e9 = Edge(dt_decision_3, dt_utility_4, name="green") - dt_utility_5 = UtilityNode("v", "Utility", utility=None) - dt_e10 = Edge(dt_decision_3, dt_utility_5, name="red") - - dt_decision_4 = DecisionNode("D", "D", alternatives=["green", "red"]) - dt_e11 = Edge(dt_uncertainty_u2_2, dt_decision_4, name="no") - dt_utility_6 = UtilityNode("v", "Utility", utility=None) - dt_e12 = Edge(dt_decision_4, dt_utility_6, name="green") - dt_utility_7 = UtilityNode("v", "Utility", utility=None) - dt_e13 = Edge(dt_decision_4, dt_utility_7, name="red") - - nodes = [ - dt_uncertainty_u1, - dt_uncertainty_u2_1, - dt_decision_1, - dt_utility_0, - dt_utility_1, - dt_decision_2, - dt_utility_2, - dt_utility_3, - dt_uncertainty_u2_2, - dt_decision_3, - dt_utility_4, - dt_utility_5, - dt_decision_4, - dt_utility_6, - dt_utility_7, - ] - net = { - "nodes": nodes, - "edges": [ - dt_e0, - dt_e1, - dt_e2, - dt_e3, - dt_e4, - dt_e5, - dt_e6, - dt_e7, - dt_e8, - dt_e9, - dt_e10, - dt_e11, - dt_e12, - dt_e13, - ], - } - - DT = DecisionTree(root=dt_uncertainty_u1) - for node in net["nodes"]: - DT.add_node(node) - for edge in net["edges"]: - DT.add_edge(edge) - - IDT = ID.convert_to_decision_tree() - for id_node, dt_node in zip(IDT.nx.nodes, DT.nx.nodes, strict=False): - assert type(id_node) is type(dt_node) - assert id_node.description == dt_node.description - if not isinstance(id_node, UtilityNode): - assert id_node.shortname == dt_node.shortname - - for id_edge, dt_edge in zip(IDT.nx.edges, DT.nx.edges, strict=False): - assert type(id_edge) is type(dt_edge) - assert id_edge[0].description == dt_edge[0].description - assert id_edge[1].description == dt_edge[1].description - assert ( - IDT.nx.edges[id_edge[0], id_edge[1]]["name"] - == DT.nx.edges[dt_edge[0], dt_edge[1]]["name"] - ) - assert ( - IDT.nx.edges[id_edge[0], id_edge[1]]["arc_type"] - == DT.nx.edges[dt_edge[0], dt_edge[1]]["arc_type"] - ) - - -def test_to_pyagrum_used_car_buyer_success(copy_testdata_tmpdir, tmp_path): - copy_testdata_tmpdir(TESTDATA) - with open(tmp_path / "id_used_car_buyer.json") as f: - json_stream = json.load(f) - influence_diagram_response = InfluenceDiagramResponse( - vertices=json_stream["vertices"], edges=json_stream["edges"] - ) - ID = InfluenceDiagram.from_db(influence_diagram_response) - - result = ID.to_pyagrum() - assert isinstance(result, gum.InfluenceDiagram) - assert result.size() == 5 - assert result.chanceNodeSize() == 2 - assert result.decisionNodeSize() == 2 - assert result.utilityNodeSize() == 1 - - # import pyAgrum.lib.image as gimg - # from shutil import copyfile - # gimg.export(result, "test2.png") - # result.saveBIFXML("test.bifxml") - # testroot = '/tmp/pytest-of-codespace/pytest-current/' - # testpath = testroot+'test_to_pyagrum_used_car_buyercurrent/' - # copyfile(testpath+"test2.png", "/workspaces/dot/test2.png") - # copyfile(testpath+"test.bifxml", "/workspaces/dot/test.bifxml") - - -def test_to_pyagrum_used_car_buyer_not_acyclic_fail( - caplog, copy_testdata_tmpdir, tmp_path -): - copy_testdata_tmpdir(TESTDATA) - with open(tmp_path / "id_used_car_buyer.json") as f: - json_stream = json.load(f) - influence_diagram_response = InfluenceDiagramResponse( - vertices=json_stream["vertices"], edges=json_stream["edges"] - ) - ID = InfluenceDiagram.from_db(influence_diagram_response) - node0 = list(ID.nx.nodes)[0] - node1 = list(ID.nx.nodes)[1] - edge1 = Edge(node0, node1, name="cyclic") - edge2 = Edge(node1, node0, name="cyclic") - ID.add_edge(edge1) - ID.add_edge(edge2) - - with pytest.raises(Exception) as exc_info: - ID.to_pyagrum() - assert [r.msg for r in caplog.records] == ["the influence diagram is not acyclic."] - assert str(exc_info.value) == "the influence diagram is not acyclic." - - -def test_to_pyagrum_used_car_buyer_uncertainty_fail( - caplog, copy_testdata_tmpdir, tmp_path -): - copy_testdata_tmpdir(TESTDATA) - with open(tmp_path / "id_used_car_buyer.json") as f: - json_stream = json.load(f) - influence_diagram_response = InfluenceDiagramResponse( - vertices=json_stream["vertices"], edges=json_stream["edges"] - ) - node_index = [ - k - for k, node in enumerate(influence_diagram_response.vertices) - if node.category == "Uncertainty" - ][0] - influence_diagram_response.vertices[node_index].probabilities = None - ID = InfluenceDiagram.from_db(influence_diagram_response) - - error = ( - "[pyAgrum] Invalid argument: Empty variable State:Labelized({}) " - "cannot be added in a Potential" - ) - with pytest.raises(Exception) as exc_info: - ID.to_pyagrum() - assert [r.msg for r in caplog.records] == [ - f"Input probability cannot be used in pyagrum with error: {error}" - ] - assert ( - str(exc_info.value) - == f"Input probability cannot be used in pyagrum with error: {error}" - ) - - -def test_to_pyagrum_used_car_buyer_decision_fail(caplog, copy_testdata_tmpdir, tmp_path): - copy_testdata_tmpdir(TESTDATA) - with open(tmp_path / "id_used_car_buyer.json") as f: - json_stream = json.load(f) - influence_diagram_response = InfluenceDiagramResponse( - vertices=json_stream["vertices"], edges=json_stream["edges"] - ) - node_index = [ - k - for k, node in enumerate(influence_diagram_response.vertices) - if node.category == "Decision" - ][0] - influence_diagram_response.vertices[node_index].alternatives = None - ID = InfluenceDiagram.from_db(influence_diagram_response) - - error = ( - "[pyAgrum] Invalid argument: Empty variable Buy:Labelized({}) " - "cannot be added in a Potential" - ) - with pytest.raises(Exception) as exc_info: - ID.to_pyagrum() - assert [r.msg for r in caplog.records] == [ - f"Input arc cannot be used in pyagrum with error: {error}" - ] - assert ( - str(exc_info.value) == f"Input arc cannot be used in pyagrum with error: {error}" - ) - - # def test_convert_to_decision_tree_asymemetry(): - - -# # Wildcatter example -# # Data taken from -# # An improved method for solving Hybrid Influence Diagrams -# # Barbaros Yet, Martin Neil, Norman Fenton, Anthony Constantinou, -# # Eugene Dementiev -# # International Journal of Approximate Reasoning, Volume 95, April 2018, -# # Pages 93-112 -# # https://doi.org/10.1016/j.ijar.2018.01.006 -# # -# # -# # Probabilities in the ID to be updated!!! -# # -# # The produced tree may be flipped horizontally compared to the expected one -# id_decision_T = DecisionNode("Seismic Test", "T", alternatives=["yes", "No"]) -# id_decision_D = DecisionNode("Drill", "D", alternatives=["yes", "No"]) -# id_uncertainty_R = UncertaintyNode("Test Result", "R", \ -# probabilities={'No': 0.410, 'Open': 0.350, 'Closed': 0.240}) -# id_uncertainty_O = UncertaintyNode("Oil", "O", \ -# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) -# id_value_S = UtilityNode("Seismic Cost", "U1", utility=0) -# id_value_D = UtilityNode("Drilling Gain", "U2", utility=0) -# id_value_T = UtilityNode("Total", "U3", utility=0) - -# id_edge_0 = Edge(id_decision_T, id_value_S) -# id_edge_1 = Edge(id_decision_T, id_uncertainty_R) -# id_edge_2 = Edge(id_decision_T, id_decision_D) -# id_edge_3 = Edge(id_uncertainty_R, id_decision_D) -# id_edge_4 = Edge(id_uncertainty_O, id_uncertainty_R) -# id_edge_5 = Edge(id_uncertainty_O, id_value_D) -# id_edge_6 = Edge(id_decision_D, id_value_D) -# id_edge_7 = Edge(id_value_S, id_value_T) -# id_edge_8 = Edge(id_value_D, id_value_T) - -# net = {'nodes': [id_decision_T, id_decision_D, id_uncertainty_R, \ -# id_uncertainty_O, id_value_S, id_value_D, id_value_T], -# 'edges': [id_edge_0, id_edge_1, id_edge_2, id_edge_3, id_edge_4, \ -# id_edge_5, id_edge_6, id_edge_7, id_edge_8]} - -# ID = InfluenceDiagram() -# for node in net['nodes']: -# ID.add_node(node) -# for edge in net['edges']: -# ID.add_edge(edge) - - -# dt_decision_T_0 = DecisionNode("Seismic Test", "T", alternatives=["yes", "No"]) -# dt_decision_R_0 = UncertaintyNode("Test Result", "R", \ -# probabilities={'No': 0.410, 'Open': 0.350, 'Closed': 0.240}) -# dt_edge_0 = Edge(dt_decision_T_0, dt_decision_R_0, name="Yes") -# dt_decision_D_0 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) -# dt_edge_1 = Edge(dt_decision_R_0, dt_decision_D_0, name="No") -# dt_uncertainty_O_0 = UncertaintyNode("Oil", "O", \ -# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) -# dt_edge_2 = Edge(dt_decision_D_0, dt_uncertainty_O_0, name="Yes") -# dt_value_T_0 = UtilityNode("Total", "U3", utility=0) -# dt_edge_3 = Edge(dt_uncertainty_O_0, dt_value_T_0, name="Dry") -# dt_value_T_1 = UtilityNode("Total", "U3", utility=0) -# dt_edge_4 = Edge(dt_uncertainty_O_0, dt_value_T_1, name="Wet") -# dt_value_T_2 = UtilityNode("Total", "U3", utility=0) -# dt_edge_5 = Edge(dt_uncertainty_O_0, dt_value_T_2, name="Soaking") -# dt_value_T_3 = UtilityNode("Total", "U3", utility=0) -# dt_edge_6 = Edge(dt_decision_D_0, dt_value_T_3, name="No") -# dt_decision_D_1 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) -# dt_edge_7 = Edge(dt_decision_R_0, dt_decision_D_1, name="Open") -# dt_uncertainty_O_1 = UncertaintyNode("Oil", "O", \ -# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) -# dt_edge_8 = Edge(dt_decision_D_1, dt_uncertainty_O_1, name="Yes") -# dt_value_T_4 = UtilityNode("Total", "U3", utility=0) -# dt_edge_9 = Edge(dt_uncertainty_O_1, dt_value_T_4, name="Dry") -# dt_value_T_5 = UtilityNode("Total", "U3", utility=0) -# dt_edge_10 = Edge(dt_uncertainty_O_1, dt_value_T_5, name="Wet") -# dt_value_T_6 = UtilityNode("Total", "U3", utility=0) -# dt_edge_11 = Edge(dt_uncertainty_O_1, dt_value_T_6, name="Soaking") -# dt_value_T_7 = UtilityNode("Total", "U3", utility=0) -# dt_edge_12 = Edge(dt_decision_D_1, dt_value_T_7, name="No") -# dt_decision_D_2 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) -# dt_edge_13 = Edge(dt_decision_R_0, dt_decision_D_2, name="Closed") -# dt_uncertainty_O_2 = UncertaintyNode("Oil", "O", \ -# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) -# dt_edge_14 = Edge(dt_decision_D_2, dt_uncertainty_O_2, name="Yes") -# dt_value_T_8 = UtilityNode("Total", "U3", utility=0) -# dt_edge_15 = Edge(dt_uncertainty_O_2, dt_value_T_8, name="Dry") -# dt_value_T_9 = UtilityNode("Total", "U3", utility=0) -# dt_edge_16 = Edge(dt_uncertainty_O_2, dt_value_T_9, name="Wet") -# dt_value_T_10 = UtilityNode("Total", "U3", utility=0) -# dt_edge_17 = Edge(dt_uncertainty_O_2, dt_value_T_10, name="Soaking") -# dt_value_T_11 = UtilityNode("Total", "U3", utility=0) -# dt_edge_18 = Edge(dt_decision_D_2, dt_value_T_11, name="No") -# dt_decision_D_3 = DecisionNode("Drill", "D", alternatives=["yes", "No"]) -# dt_edge_19 = Edge(dt_decision_T_0, dt_decision_D_3, name="No") -# dt_uncertainty_O_3 = UncertaintyNode("Oil", "O", \ -# probabilities={'Dry': 0.7, 'Wet': 0.2, 'Soaking': 0.1}) -# dt_edge_20 = Edge(dt_decision_D_2, dt_uncertainty_O_3, name="Yes") -# dt_value_T_12 = UtilityNode("Total", "U3", utility=0) -# dt_edge_21 = Edge(dt_uncertainty_O_3, dt_value_T_12, name="Dry") -# dt_value_T_13 = UtilityNode("Total", "U3", utility=0) -# dt_edge_22 = Edge(dt_uncertainty_O_3, dt_value_T_13, name="Wet") -# dt_value_T_14 = UtilityNode("Total", "U3", utility=0) -# dt_edge_23 = Edge(dt_uncertainty_O_3, dt_value_T_14, name="Soaking") -# dt_value_T_15 = UtilityNode("Total", "U3", utility=0) -# dt_edge_24 = Edge(dt_decision_D_3, dt_value_T_15, name="No") - - -# net = {'nodes': [dt_decision_T_0, -# dt_decision_R_0, -# dt_decision_D_0, -# dt_uncertainty_O_0, -# dt_value_T_0, -# dt_value_T_1, -# dt_value_T_2, -# dt_value_T_3, -# dt_decision_D_1, -# dt_uncertainty_O_1, -# dt_value_T_4, -# dt_value_T_5, -# dt_value_T_6, -# dt_value_T_7, -# dt_decision_D_2, -# dt_uncertainty_O_2, -# dt_value_T_8, -# dt_value_T_9, -# dt_value_T_10, -# dt_value_T_11, -# dt_decision_D_3, -# dt_uncertainty_O_3, -# dt_value_T_12, -# dt_value_T_13, -# dt_value_T_14, -# dt_value_T_15], -# 'edges': [dt_edge_0, -# dt_edge_1, -# dt_edge_2, -# dt_edge_3, -# dt_edge_4, -# dt_edge_5, -# dt_edge_6, -# dt_edge_7, -# dt_edge_8, -# dt_edge_8, -# dt_edge_9, -# dt_edge_10, -# dt_edge_11, -# dt_edge_12, -# dt_edge_13, -# dt_edge_14, -# dt_edge_15, -# dt_edge_16, -# dt_edge_17, -# dt_edge_18, -# dt_edge_19, -# dt_edge_20, -# dt_edge_21, -# dt_edge_22, -# dt_edge_23, -# dt_edge_24]} - -# DT = DecisionTree() -# for node in net['nodes']: -# DT.add_node(node) -# for edge in net['edges']: -# DT.add_edge(edge) - -# assert ID.convert_to_decision_tree() == DT diff --git a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_node.py b/api/tests/v0/services/structure_utils/test_decision_diagrams/test_node.py deleted file mode 100644 index 6287d40..0000000 --- a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_node.py +++ /dev/null @@ -1,340 +0,0 @@ -from unittest.mock import patch - -import pytest - -from src.v0.models.issue import IssueResponse -from src.v0.services.structure_utils.decision_diagrams.node import ( - DecisionNode, - NodeABC, - UncertaintyNode, - UtilityNode, -) -from src.v0.services.structure_utils.probability.discrete_unconditional_probability import ( # noqa: E501 - DiscreteUnconditionalProbability, -) - - -def test_class_NodeABC(monkeypatch): - monkeypatch.setattr(NodeABC, "__abstractmethods__", set()) - with pytest.raises(NotImplementedError): - NodeABC._from_db_model() - - with pytest.raises(NotImplementedError): - states = NodeABC("J", "J").states # assignment to variables only for ruff - assert states - - -def test_NodeABC(monkeypatch): - monkeypatch.setattr(NodeABC, "__abstractmethods__", set()) - with pytest.raises(NotImplementedError): - NodeABC.get_instance_input(None) - - -def test_class_DecisionNode(): - node = DecisionNode("J", "junk") - assert node.description == "junk" - assert node.shortname == "J" - assert isinstance(node.uuid, str) - assert len(node.uuid) == 36 - assert isinstance(node.alternatives, list) and not node.alternatives - assert isinstance(node.states, list) and not node.states - assert node.is_decision_node - assert not node.is_uncertainty_node - assert not node.is_utility_node - - -def test_class_UncertaintyNode(): - node = UncertaintyNode("J", "junk") - assert node.description == "junk" - assert node.shortname == "J" - assert isinstance(node.uuid, str) - assert len(node.uuid) == 36 - assert node.probabilities is None - assert isinstance(node.outcomes, tuple) and not node.outcomes - assert isinstance(node.states, tuple) and not node.states - assert not node.is_decision_node - assert node.is_uncertainty_node - assert not node.is_utility_node - - -def test_class_UtilityNode(): - node = UtilityNode("J", "junk") - assert node.description == "junk" - assert node.shortname == "J" - assert isinstance(node.uuid, str) - assert len(node.uuid) == 36 - assert isinstance(node.utility, list) and not node.utility - assert isinstance(node.states, list) and not node.states - assert not node.is_decision_node - assert not node.is_uncertainty_node - assert node.is_utility_node - - -def test_copy(): - node = DecisionNode("J", "junk", alternatives=["a0", "a1", "a2"]) - copied_node = node.copy() - assert isinstance(copied_node, type(node)) - assert copied_node.description == node.description - assert copied_node.shortname == node.shortname - assert copied_node.alternatives == node.alternatives - - -@patch("src.v0.services.structure_utils.decision_diagrams.node.uuid4") -def test_DecisionNode_to_dict(uuid_mocker): - uuid_mocker.return_value = "mocked_uuid" - node = DecisionNode("J", "junk") - node.__junk = 3.14 - node.junky = "Coke" - assert node.to_dict() == { - "node_type": "DecisionNode", - "description": "junk", - "shortname": "J", - "uuid": "mocked_uuid", - "alternatives": [], - "junk": None, - "junky": "Coke", - } - - -@patch("src.v0.services.structure_utils.decision_diagrams.node.uuid4") -def test_UncertaintyNode_to_dict(uuid_mocker): - uuid_mocker.return_value = "mocked_uuid" - probabilities = DiscreteUnconditionalProbability( - probability_function=[[0.5, 0.5], [0.4, 0.6]], - variables={ - "Node1": ["Outcome1", "Outcome2"], - "Node2": ["Outcome21", "Outcome22"], - }, - ) - node = UncertaintyNode("J", "junk", probabilities=probabilities) - assert node.to_dict() == { - "node_type": "UncertaintyNode", - "description": "junk", - "shortname": "J", - "uuid": "mocked_uuid", - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.5, 0.5], [0.4, 0.6]], - "variables": { - "Node1": ["Outcome1", "Outcome2"], - "Node2": ["Outcome21", "Outcome22"], - }, - }, - } - - -@patch("src.v0.services.structure_utils.decision_diagrams.node.uuid4") -def test_UtilityNode_to_dict(uuid_mocker): - uuid_mocker.return_value = "mocked_uuid" - node = UtilityNode("J", "junk") - assert node.to_dict() == { - "node_type": "UtilityNode", - "description": "junk", - "shortname": "J", - "uuid": "mocked_uuid", - "utility": [], - } - - -def test_from_dict(): - node = {"node_type": "DecisionNode", "description": "junk", "shortname": "J"} - node = NodeABC.from_dict(node) - assert node.description == "junk" - assert node.shortname == "J" - assert isinstance(node.uuid, str) - assert len(node.uuid) == 36 - assert isinstance(node.alternatives, list) and not node.alternatives - assert node.is_decision_node - assert not node.is_uncertainty_node - assert not node.is_utility_node - - -def test_from_dict_fail(caplog): - node = {"node_type": "UnknownNode", "description": "junk", "shortname": "J"} - with pytest.raises(Exception) as exc_info: - NodeABC.from_dict(node) - assert [r.msg for r in caplog.records] == ["failing instantiation of UnknownNode"] - assert str(exc_info.value) == "failing instantiation of UnknownNode" - - -def test_from_db_uncertainty_node_2d_unconditional(): - json_object = { - "tag": ["State"], - "category": "Uncertainty", - "index": "0", - "description": "Joe does not know the state of the car", - "shortname": "State", - "keyUncertainty": "true", - "decisionType": "", - "alternatives": ["Peach", "Lemon"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.5, 0.5], [0.4, 0.6]], - "variables": { - "Node1": ["Outcome1", "Outcome2"], - "Node2": ["Outcome21", "Outcome22"], - }, - }, - "influenceNodeUUID": "", - "boundary": "", - "comments": [{"comment": "", "author": ""}], - "uuid": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "timestamp": "1712648453.1573343", - "date": "2024-04-09 07:40:53.157336", - "ids": "test", - "id": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "label": "issue", - } - - response = IssueResponse(**json_object) - result = NodeABC.from_db(response) - assert isinstance(result, UncertaintyNode) - assert result.description == "Joe does not know the state of the car" - assert result.shortname == "State" - assert result.uuid == "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e" - assert isinstance(result.probabilities, DiscreteUnconditionalProbability) - assert result.states == ( - ("Outcome1", "Outcome21"), - ("Outcome1", "Outcome22"), - ("Outcome2", "Outcome21"), - ("Outcome2", "Outcome22"), - ) - assert ( - result.probabilities.get_distribution(Node1="Outcome1", Node2="Outcome21") == 0.5 - ) - - -def test_from_db_uncertainty_node_1d_unconditional(): - json_object = { - "tag": ["State"], - "category": "Uncertainty", - "index": "0", - "description": "Joe does not know the state of the car", - "shortname": "State", - "keyUncertainty": "true", - "decisionType": "", - "alternatives": ["Peach", "Lemon"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.6, 0.4]], - "variables": {"Node1": ["Outcome1", "Outcome2"]}, - }, - "influenceNodeUUID": "", - "boundary": "", - "comments": [{"comment": "", "author": ""}], - "uuid": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "timestamp": "1712648453.1573343", - "date": "2024-04-09 07:40:53.157336", - "ids": "test", - "id": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "label": "issue", - } - - response = IssueResponse(**json_object) - result = NodeABC.from_db(response) - assert isinstance(result, UncertaintyNode) - assert result.description == "Joe does not know the state of the car" - assert result.shortname == "State" - assert result.uuid == "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e" - assert isinstance(result.probabilities, DiscreteUnconditionalProbability) - assert result.states == ("Outcome1", "Outcome2") - assert result.probabilities.get_distribution(Node1="Outcome1") == 0.6 - - -def test_from_db_decision_node(): - json_object = { - "tag": ["Test"], - "category": "Decision", - "index": "0", - "description": "Joe can test the car", - "shortname": "Test", - "keyUncertainty": "false", - "decisionType": "Focus", - "alternatives": ["Test", "no Test"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.6, 0.4]], - "variables": {"Node1": ["Outcome1", "Outcome2"]}, - }, - "influenceNodeUUID": "", - "boundary": "", - "comments": [{"comment": "", "author": ""}], - "uuid": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "timestamp": "1712648468.41026", - "date": "2024-04-09 07:41:08.410262", - "ids": "test", - "id": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "label": "issue", - } - response = IssueResponse(**json_object) - result = NodeABC.from_db(response) - assert isinstance(result, DecisionNode) - assert result.description == "Joe can test the car" - assert result.shortname == "Test" - assert result.uuid == "ad651f50-22de-4f85-a560-bf5fb2d9f706" - assert result.states == ["Test", "no Test"] - assert result.alternatives == ["Test", "no Test"] - - -def test_from_db_utility_node(): - json_object = { - "tag": ["Value"], - "category": "Value Metric", - "index": "0", - "description": "Value", - "shortname": "Value", - "keyUncertainty": "false", - "decisionType": "", - "alternatives": ["Test"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.6, 0.4]], - "variables": {"Node1": ["Outcome1", "Outcome2"]}, - }, - "influenceNodeUUID": "", - "boundary": "", - "comments": [{"comment": "", "author": ""}], - "uuid": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "timestamp": "1712648501.9647892", - "date": "2024-04-09 07:41:41.964793", - "ids": "test", - "id": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "label": "issue", - } - response = IssueResponse(**json_object) - result = NodeABC.from_db(response) - assert isinstance(result, UtilityNode) - assert result.description == "Value" - assert result.shortname == "Value" - assert result.uuid == "d52cadfd-c3b8-4531-8e3b-b7e966271edb" - - -def test_from_db_fail_node_type(): - json_object = { - "tag": ["Value"], - "category": "Junky", - "index": "0", - "description": "Value", - "shortname": "Value", - "keyUncertainty": "false", - "decisionType": "", - "alternatives": ["Test"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.6, 0.4]], - "variables": {"Node1": ["Outcome1", "Outcome2"]}, - }, - "influenceNodeUUID": "", - "boundary": "", - "comments": [{"comment": "", "author": ""}], - "uuid": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "timestamp": "1712648501.9647892", - "date": "2024-04-09 07:41:41.964793", - "ids": "test", - "id": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "label": "issue", - } - response = IssueResponse(**json_object) - with pytest.raises(Exception) as exc: - NodeABC.from_db(response) - assert str(exc.value) == "failing instantiation of JunkyNode" diff --git a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_probabilistic_graph_model.py b/api/tests/v0/services/structure_utils/test_decision_diagrams/test_probabilistic_graph_model.py deleted file mode 100644 index bc1a4d0..0000000 --- a/api/tests/v0/services/structure_utils/test_decision_diagrams/test_probabilistic_graph_model.py +++ /dev/null @@ -1,14 +0,0 @@ -import pytest - -from src.v0.services.structure_utils.decision_diagrams.probabilistic_graph_model import ( - ProbabilisticGraphModelABC, -) - - -def test_class_ProbabilisticGraphModelABC(monkeypatch): - monkeypatch.setattr(ProbabilisticGraphModelABC, "__abstractmethods__", set()) - node = ProbabilisticGraphModelABC() - with pytest.raises(NotImplementedError): - node.initialize_diagram(None) - with pytest.raises(NotImplementedError): - node._to_json_stream() diff --git a/api/tests/v0/services/structure_utils/test_probability/test_abstract_probability.py b/api/tests/v0/services/structure_utils/test_probability/test_abstract_probability.py deleted file mode 100644 index 11f479e..0000000 --- a/api/tests/v0/services/structure_utils/test_probability/test_abstract_probability.py +++ /dev/null @@ -1,46 +0,0 @@ -import pytest - -from src.v0.services.structure_utils.probability.abstract_probability import ( - ProbabilityABC, -) - - -def test_class_ProbabilityABC(monkeypatch): - monkeypatch.setattr( - ProbabilityABC, - "__abstractmethods__", - set(), - ) - abstract_probability = ProbabilityABC() - with pytest.raises(NotImplementedError): - ProbabilityABC.from_db_model() - - with pytest.raises(NotImplementedError): - abstract_probability.initialize_nan() - - with pytest.raises(NotImplementedError): - abstract_probability.set_to_uniform() - - with pytest.raises(NotImplementedError): - abstract_probability.normalize() - - with pytest.raises(NotImplementedError): - abstract_probability.isnormalized() - - with pytest.raises(NotImplementedError): - abstract_probability.from_json() - - with pytest.raises(NotImplementedError): - abstract_probability.to_json() - - with pytest.raises(NotImplementedError): - abstract_probability.to_dict() - - with pytest.raises(NotImplementedError): - abstract_probability.get_distribution() - - with pytest.raises(NotImplementedError): - abstract_probability.to_pyagrum() - - with pytest.raises(NotImplementedError): - abstract_probability.to_pycid() diff --git a/api/tests/v0/services/structure_utils/test_probability/test_discrete_conditional_probability.py b/api/tests/v0/services/structure_utils/test_probability/test_discrete_conditional_probability.py deleted file mode 100644 index c92a157..0000000 --- a/api/tests/v0/services/structure_utils/test_probability/test_discrete_conditional_probability.py +++ /dev/null @@ -1,205 +0,0 @@ -import numpy as np -import pytest -import xarray as xr - -from src.v0.models.issue import ProbabilityData -from src.v0.services.structure_utils.probability.discrete_conditional_probability import ( # noqa: E501 - DiscreteConditionalProbability, -) - - -@pytest.fixture -def cpt_2d(): - values = np.array([[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]]) - coords = {"A": ["yes", "no"], "B": ["low", "mid", "high"]} - return DiscreteConditionalProbability(values, coords) - - -@pytest.fixture -def cpt_3d(): - data = ProbabilityData( - **{ - "dtype": "DiscreteConditionalProbability", - "probability_function": [ - [0.1, 0.05, 0.85, 0.46], - [0.7, 0.35, 0.12, 0.26], - [0.2, 0.60, 0.03, 0.28], - ], - "variables": { - "Test Result": ["no Test", "Peach", "Lemon"], - "Test": ["yes", "no"], - "State": ["Peach", "Lemon"], - }, - } - ) - return DiscreteConditionalProbability.from_db_model(data) - - -def test_class_discrete_conditional_probability(cpt_2d): - assert isinstance(cpt_2d._cpt, xr.DataArray) - np.testing.assert_equal(cpt_2d._cpt.sel(A="yes"), np.array([0.1, 0.7, 0.6])) - np.testing.assert_equal(cpt_2d._cpt.sel(A="no"), np.array([0.9, 0.3, 0.4])) - np.testing.assert_equal(cpt_2d._cpt.sel(A="yes").sel(B="mid"), np.array([0.7])) - - -def test_outcomes(cpt_2d): - assert cpt_2d.outcomes == ("yes", "no") - - -def test_variables(cpt_2d): - assert cpt_2d.variables == ("A", "B") - - -def test_from_db(): - data = ProbabilityData( - **{ - "dtype": "DiscreteConditionalProbability", - "probability_function": [ - [0.1, 0.05, 0.85, 0.46], - [0.7, 0.35, 0.12, 0.26], - [0.2, 0.60, 0.03, 0.28], - ], - "variables": { - "Test Result": ["no Test", "Peach", "Lemon"], - "Test": ["yes", "no"], - "State": ["Peach", "Lemon"], - }, - } - ) - result = DiscreteConditionalProbability.from_db_model(data) - assert result._cpt.shape == (3, 2, 2) - assert result.get_distribution(TestResult="Peach", Test="yes", State="Peach") == 0.7 - assert result.get_distribution(TestResult="Lemon", Test="yes", State="Peach") == 0.2 - assert result.get_distribution(TestResult="Lemon", Test="yes", State="Lemon") == 0.6 - assert result.get_distribution(TestResult="Lemon", Test="no", State="Peach") == 0.03 - assert result.get_distribution(TestResult="Lemon", Test="no", State="Lemon") == 0.28 - - -def test_initialize_nan(): - coords = { - "a": np.array([1, 2, 3]), - "b": np.array([4, 5]), - "c": ["yes", "no"], - } - result = DiscreteConditionalProbability.initialize_nan(variables=coords) - assert result._cpt.shape == (3, 2, 2) - assert np.all(np.isnan(result._cpt)) - - -def test_initialize_nan_fail(): - coords = { - "a": np.array([1, 2, 3]), - "b": np.array([[4, 5, 6], [7, 8, 9]]), - "c": ["yes", "no"], - } - with pytest.raises(Exception) as exc: - DiscreteConditionalProbability.initialize_nan(variables=coords) - assert str(exc.value) == "One of the variables cannot be interpreted as 1D" - - -def test_set_to_uniform(cpt_2d): - cpt_2d.set_to_uniform() - assert np.all(cpt_2d._cpt == 0.5) - - -def test_normalize(): - values = np.array([[0.2, 0.2], [0.8, 0.2], [0.6, 0.4]]) - coords = {"A": ["low", "mid", "high"], "B": ["yes", "no"]} - result = DiscreteConditionalProbability(values, coords) - result.normalize() - np.testing.assert_equal(result._cpt, np.array([[0.5, 0.5], [0.8, 0.2], [0.6, 0.4]])) - - -def test_normalize_2by2(): - values = np.array([[0.2, 0.2], [0.7, 0.3]]) - coords = {"A": ["low", "high"], "B": ["yes", "no"]} - result = DiscreteConditionalProbability(values, coords) - result.normalize() - np.testing.assert_equal(result._cpt, np.array([[0.5, 0.5], [0.7, 0.3]])) - - -def test_isnormalized(cpt_2d): - assert cpt_2d.isnormalized() - - -def test_from_json(cpt_2d): - stream = """{ - "dtype": "DiscreteConditionalProbability", - "probability_function": [[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]], - "variables": { - "A": ["yes", "no"], - "B": ["low", "mid", "high"] - } - }""" - result = DiscreteConditionalProbability.from_json(stream) - xr.testing.assert_allclose(result._cpt, cpt_2d._cpt) - - -def test_from_json_fail(cpt_2d): - stream = """{ - "dtype": "Junk", - "probability_function": [[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]], - "variables": { - "A": ["yes", "no"], - "B": ["low", "mid", "high"] - } - }""" - with pytest.raises(Exception) as exc: - DiscreteConditionalProbability.from_json(stream) - assert str(exc.value) == "Expected DiscreteConditionalProbability dtype, got Junk" - - -def test_to_json(cpt_2d): - assert cpt_2d.to_json() == ( - '{"dtype": "DiscreteConditionalProbability", ' - '"probability_function": [[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]], ' - '"variables": {"A": ["yes", "no"], "B": ["low", "mid", "high"]}}' - ) - - -def test_get_distribution(cpt_2d): - np.testing.assert_allclose( - cpt_2d.get_distribution(A="yes"), np.array([0.1, 0.7, 0.6]) - ) - assert cpt_2d.get_distribution(A="no", B="mid") == 0.3 - - -def test_add_conditioning_variable(cpt_2d): - with pytest.raises(NotImplementedError): - cpt_2d.add_conditioning_variable(None) - - -def test_remove_conditioning_variable(cpt_2d): - with pytest.raises(NotImplementedError): - cpt_2d.remove_conditioning_variable(None) - - -def test_add_na_outcomes(cpt_2d): - with pytest.raises(NotImplementedError): - cpt_2d.add_na_outcomes() - - -def test_to_pyagrum_2d(cpt_2d): - result = cpt_2d.to_pyagrum() - target = [ - ({"B": 0}, [0.1, 0.9]), - ({"B": 1}, [0.7, 0.3]), - ({"B": 2}, [0.6, 0.4]), - ] - assert result == target - - -def test_to_pyagrum_3d(cpt_3d): - result = cpt_3d.to_pyagrum() - target = [ - ({"Test": 0, "State": 0}, [0.10, 0.70, 0.20]), - ({"Test": 0, "State": 1}, [0.05, 0.35, 0.60]), - ({"Test": 1, "State": 0}, [0.85, 0.12, 0.03]), - ({"Test": 1, "State": 1}, [0.46, 0.26, 0.28]), - ] - assert result == target - - -def test_to_pycid(cpt_2d): - with pytest.raises(NotImplementedError): - cpt_2d.to_pycid() diff --git a/api/tests/v0/services/structure_utils/test_probability/test_discrete_unconditional_probability.py b/api/tests/v0/services/structure_utils/test_probability/test_discrete_unconditional_probability.py deleted file mode 100644 index f928299..0000000 --- a/api/tests/v0/services/structure_utils/test_probability/test_discrete_unconditional_probability.py +++ /dev/null @@ -1,206 +0,0 @@ -import numpy as np -import pytest -import xarray as xr - -from src.v0.models.issue import ProbabilityData -from src.v0.services.structure_utils.probability.discrete_unconditional_probability import ( # noqa: E501 - DiscreteUnconditionalProbability, -) - - -@pytest.fixture -def cpt_1d(): - values = np.array([0.3, 0.5, 0.2]) - coords = {"A": ["yes", "no", "maybe"]} - return DiscreteUnconditionalProbability(values, coords) - - -@pytest.fixture -def cpt_2d(): - values = np.array([[0.1, 0.3, 0.2], [0.2, 0.1, 0.1]]) - coords = {"A": ["yes", "no"], "B": ["low", "mid", "high"]} - return DiscreteUnconditionalProbability(values, coords) - - -@pytest.fixture -def cpt_3d(): - values = np.array( - [ - [[0.02, 0.05, 0.03], [0.05, 0.02, 0.03], [0.05, 0.25, 0.05]], - [[0.03, 0.02, 0.05], [0.05, 0.1, 0.02], [0.03, 0.05, 0.1]], - ] - ) - coords = { - "A": ["yes", "no"], - "B": ["low", "mid", "high"], - "C": ["red", "green", "blue"], - } - return DiscreteUnconditionalProbability(values, coords) - - -def test_class_discrete_conditional_probability(cpt_2d): - assert isinstance(cpt_2d._cpt, xr.DataArray) - np.testing.assert_equal(cpt_2d._cpt.sel(A="yes"), np.array([0.1, 0.3, 0.2])) - np.testing.assert_equal(cpt_2d._cpt.sel(A="no"), np.array([0.2, 0.1, 0.1])) - np.testing.assert_equal(cpt_2d._cpt.sel(A="yes").sel(B="mid"), np.array([0.3])) - - -def test_outcomes(cpt_2d): - assert cpt_2d.outcomes == ( - ("yes", "low"), - ("yes", "mid"), - ("yes", "high"), - ("no", "low"), - ("no", "mid"), - ("no", "high"), - ) - - -def test_variables(cpt_2d): - assert cpt_2d.variables == ("A", "B") - - -def test_from_db(): - data = ProbabilityData( - **{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [0.0, 0.0, 1.0, 1.0], - [1.0, 0.0, 0.0, 0.0], - [0.0, 1.0, 0.0, 0.0], - ], - "variables": { - "Test Result": ["no Test", "Peach", "Lemon"], - "Test": ["yes", "no"], - "State": ["Peach", "Lemon"], - }, - } - ) - result = DiscreteUnconditionalProbability.from_db_model(data) - assert result._cpt.shape == (3, 2, 2) - assert result.get_distribution(TestResult="Peach", Test="yes", State="Peach") == 1.0 - - -def test_initialize_nan(): - coords = { - "a": np.array([1, 2, 3]), - "b": np.array([4, 5]), - "c": ["yes", "no"], - } - result = DiscreteUnconditionalProbability.initialize_nan(variables=coords) - assert result._cpt.shape == (3, 2, 2) - assert np.all(np.isnan(result._cpt)) - - -def test_initialize_nan_fail(): - coords = { - "a": np.array([1, 2, 3]), - "b": np.array([[4, 5, 6], [7, 8, 9]]), - "c": ["yes", "no"], - } - with pytest.raises(Exception) as exc: - DiscreteUnconditionalProbability.initialize_nan(variables=coords) - assert str(exc.value) == "One of the variables cannot be interpreted as 1D" - - -def test_set_to_uniform(cpt_2d): - cpt_2d.set_to_uniform() - assert np.all(np.linalg.norm(cpt_2d._cpt - 1 / 6) < 1e-12) - - -def test_normalize(): - values = np.array([[0.15, 0.2], [0.1, 0.05]]) - coords = {"A": ["low", "high"], "B": ["yes", "no"]} - result = DiscreteUnconditionalProbability(values, coords) - result.normalize() - np.testing.assert_allclose(result._cpt, np.array([[0.3, 0.4], [0.2, 0.1]])) - - -def test_isnormalized(cpt_2d): - assert cpt_2d.isnormalized() - - -def test_from_json(cpt_2d): - stream = """{ - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.1, 0.3, 0.2], [0.2, 0.1, 0.1]], - "variables": { - "A": ["yes", "no"], - "B": ["low", "mid", "high"] - } - }""" - result = DiscreteUnconditionalProbability.from_json(stream) - xr.testing.assert_allclose(result._cpt, cpt_2d._cpt) - - -def test_from_json_fail(cpt_2d): - stream = """{ - "dtype": "Junk", - "probability_function": [[0.1, 0.7, 0.6], [0.9, 0.3, 0.4]], - "variables": { - "A": ["yes", "no"], - "B": ["low", "mid", "high"] - } - }""" - with pytest.raises(Exception) as exc: - DiscreteUnconditionalProbability.from_json(stream) - assert str(exc.value) == "Expected DiscreteUnconditionalProbability dtype, got Junk" - - -def test_to_json_1d(cpt_1d): - assert cpt_1d.to_json() == ( - '{"dtype": "DiscreteUnconditionalProbability", ' - '"probability_function": [[0.3], [0.5], [0.2]], ' - '"variables": {"A": ["yes", "no", "maybe"]}}' - ) - - -def test_to_json_2d(cpt_2d): - assert cpt_2d.to_json() == ( - '{"dtype": "DiscreteUnconditionalProbability", ' - '"probability_function": [[0.1, 0.3, 0.2], [0.2, 0.1, 0.1]], ' - '"variables": {"A": ["yes", "no"], "B": ["low", "mid", "high"]}}' - ) - - -def test_to_json_3d(cpt_3d): - assert cpt_3d.to_json() == ( - '{"dtype": "DiscreteUnconditionalProbability", ' - '"probability_function": ' - "[[0.02, 0.05, 0.03, 0.05, 0.02, 0.03, 0.05, 0.25, 0.05], " - "[0.03, 0.02, 0.05, 0.05, 0.1, 0.02, 0.03, 0.05, 0.1]], " - '"variables": {"A": ["yes", "no"], ' - '"B": ["low", "mid", "high"], ' - '"C": ["red", "green", "blue"]}}' - ) - - -def test_get_distribution(cpt_2d): - np.testing.assert_allclose( - cpt_2d.get_distribution(A="yes"), np.array([0.1, 0.3, 0.2]) - ) - assert cpt_2d.get_distribution(A="no", B="mid") == 0.1 - - -def test_add_na_outcomes(cpt_2d): - with pytest.raises(NotImplementedError): - cpt_2d.add_na_outcomes() - - -def test_to_pyagrum_1d(cpt_1d): - result = cpt_1d.to_pyagrum() - target = [ - ({}, [0.3, 0.5, 0.2]), - ] - assert result == target - - -def test_to_pyagrum_1d_failed(cpt_2d): - with pytest.raises(Exception) as exc: - cpt_2d.to_pyagrum() - assert str(exc.value) == "pyAgrum only takes 1D variables in UncertaintyNode" - - -def test_to_pycid(cpt_2d): - with pytest.raises(NotImplementedError): - cpt_2d.to_pycid() diff --git a/api/tests/v0/services/test_issue.py b/api/tests/v0/services/test_issue.py index 515f3f9..31df3c8 100644 --- a/api/tests/v0/services/test_issue.py +++ b/api/tests/v0/services/test_issue.py @@ -57,7 +57,7 @@ def issue(): "tag": ["junk"], "index": "1234", "category": "issue", - "keyUncertainty": "True", + "keyUncertainty": "true", "decisionType": "Tactical", "alternatives": ["yes", "no"], "probabilities": { diff --git a/api/tests/v0/services/test_structure.py b/api/tests/v0/services/test_structure.py index 41f0869..6a3ad4d 100644 --- a/api/tests/v0/services/test_structure.py +++ b/api/tests/v0/services/test_structure.py @@ -61,14 +61,14 @@ def graph(): "alternatives": [""], "probabilities": { "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.9, 0.1], [0.8, 0.2]], + "probability_function": [[0.4, 0.1], [0.3, 0.2]], "variables": {"var1": ["out1", "out2"], "var2": ["in1", "in2"]}, }, "influenceNodeUUID": "", "boundary": "in", "comments": None, - "uuid": "11-aa", - "id": "11-aa", + "uuid": "11111111-9999-4444-9999-aaaaaaaaaaaa", + "id": "11111111-9999-4444-9999-aaaaaaaaaaaa", } decision = { @@ -84,8 +84,8 @@ def graph(): "influenceNodeUUID": "", "boundary": "in", "comments": None, - "uuid": "22-bb", - "id": "22-bb", + "uuid": "22222222-9999-4444-9999-bbbbbbbbbbbb", + "id": "22222222-9999-4444-9999-bbbbbbbbbbbb", } value_metric = { @@ -101,8 +101,8 @@ def graph(): "influenceNodeUUID": "", "boundary": "in", "comments": None, - "uuid": "33-cc", - "id": "33-cc", + "uuid": "33333333-9999-4444-9999-cccccccccccc", + "id": "33333333-9999-4444-9999-cccccccccccc", } metadata = { @@ -132,15 +132,15 @@ def test_read_influence_diagram_success(mock_client, mock_edge_repository, graph EdgeResponse( uuid="101", id="101", - outV="11-aa", - inV="22-bb", + outV="11111111-9999-4444-9999-aaaaaaaaaaaa", + inV="22222222-9999-4444-9999-bbbbbbbbbbbb", label="influences", ), EdgeResponse( uuid="102", id="102", - outV="22-bb", - inV="33-cc", + outV="22222222-9999-4444-9999-bbbbbbbbbbbb", + inV="33333333-9999-4444-9999-cccccccccccc", label="influences", ), ] @@ -175,15 +175,15 @@ def test_create_decision_tree_success(mock_client, graph): EdgeResponse( uuid="101", id="101", - outV="11-aa", - inV="22-bb", + outV="11111111-9999-4444-9999-aaaaaaaaaaaa", + inV="22222222-9999-4444-9999-bbbbbbbbbbbb", label="influences", ), EdgeResponse( uuid="102", id="102", - outV="22-bb", - inV="33-cc", + outV="22222222-9999-4444-9999-bbbbbbbbbbbb", + inV="33333333-9999-4444-9999-cccccccccccc", label="influences", ), ] @@ -196,23 +196,26 @@ def test_create_decision_tree_success(mock_client, graph): with patch( "src.v0.services.structure.StructureService.read_influence_diagram" ) as mocked_id: - mocked_id.return_value = InfluenceDiagramResponse(vertices=vertices, edges=edges) + # mocked_id.return_value = InfluenceDiagramResponse(nodes=vertices, edges=edges) + nodes = [n.model_dump() for n in vertices] + arcs = [a.model_dump() for a in edges] + mocked_id.return_value = InfluenceDiagramResponse(vertices=nodes, edges=arcs) result = service.create_decision_tree(project_uuid="0") assert result.children[0].children[0].children is None assert len(result.children[0].children) == 2 assert len(result.children) == 4 assert result.id.model_dump() == { - "node_type": "UncertaintyNode", + "node_type": "Uncertainty", "shortname": "Issue ABC", "alternatives": None, "description": "Bla", "probabilities": { "dtype": "DiscreteUnconditionalProbability", - "probability_function": [[0.9, 0.1], [0.8, 0.2]], + "probability_function": [[0.4, 0.1], [0.3, 0.2]], "variables": {"var1": ["out1", "out2"], "var2": ["in1", "in2"]}, }, "branch_name": "", "utility": None, - "uuid": "11-aa", + "uuid": "11111111-9999-4444-9999-aaaaaaaaaaaa", } diff --git a/api/tests/v0/services/testdata/id_used_car_buyer.json b/api/tests/v0/services/testdata/id_used_car_buyer.json deleted file mode 100644 index 591b167..0000000 --- a/api/tests/v0/services/testdata/id_used_car_buyer.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "vertices": [ - { - "tag": ["State"], - "category": "Uncertainty", - "index": "0", - "shortname": "State", - "description": "Joe does not know the state of the car", - "keyUncertainty": "true", - "decisionType": "", - "alternatives": ["Peach", "Lemon"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.8, - 0.2 - ] - ], - "variables": { - "State": [ - "Peach", - "Lemon" - ] - } - }, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "timestamp": "1712648453.1573343", - "date": "2024-04-09 07:40:53.157336", - "ids": "test", - "id": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "label": "issue" - }, - { - "tag": ["Test Result"], - "category": "Uncertainty", - "index": "0", - "description": "The result of the test is currently unknown", - "shortname": "Test Result", - "keyUncertainty": "true", - "decisionType": "", - "alternatives": ["no Test", "Peach", "Lemon"], - "probabilities": { - "dtype": "DiscreteConditionalProbability", - "probability_function": [ - [ - 0.0, - 0.0, - 1.0, - 1.0 - ], - [ - 1.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 1.0, - 0.0, - 0.0 - ] - ], - "variables": { - "Test Result": [ - "no Test", - "Peach", - "Lemon" - ], - "Test": [ - "yes", - "no" - ], - "State": [ - "Peach", - "Lemon" - ] - } - }, - "influenceNodeUUID": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "boundary": "in", - "comments": null, - "uuid": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "timestamp": "1712648489.467934", - "date": "2024-04-09 07:41:29.467935", - "ids": "test", - "id": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "label": "issue" - }, - { - "tag": ["Buy"], - "category": "Decision", - "index": "0", - "description": "We can buy the car", - "shortname": "Buy", - "keyUncertainty": "false", - "decisionType": "Focus", - "alternatives": ["Buy with guarantee", "Buy without guarantee", "Do not buy"], - "probabilities": null, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "98e3d193-d830-452f-9fe8-c21d258ef603", - "timestamp": "1712648530.228483", - "date": "2024-04-09 07:42:10.228485", - "ids": "test", - "id": "98e3d193-d830-452f-9fe8-c21d258ef603", - "label": "issue" - }, - { - "tag": ["Test"], - "category": "Decision", - "index": "0", - "description": "Joe can test the car", - "shortname": "Test", - "keyUncertainty": "false", - "decisionType": "Focus", - "alternatives": ["Test", "no Test"], - "probabilities": null, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "timestamp": "1712648468.41026", - "date": "2024-04-09 07:41:08.410262", - "ids": "test", - "id": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "label": "issue" - }, - { - "tag": ["Value"], - "category": "Value Metric", - "index": "0", - "description": "Value", - "shortname": "Value", - "keyUncertainty": "false", - "decisionType": "", - "alternatives": ["Test"], - "probabilities": null, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "timestamp": "1712648501.9647892", - "date": "2024-04-09 07:41:41.964793", - "ids": "test", - "id": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "label": "issue" - } - ], - "edges": [ - { - "id": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", - "outV": "98e3d193-d830-452f-9fe8-c21d258ef603", - "inV": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", - "label": "influences" - }, - { - "id": "44e4aed1-ec53-4d9c-9401-5204d913d7a5", - "outV": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "inV": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "uuid": "44e4aed1-ec53-4d9c-9401-5204d913d7a5", - "label": "influences" - }, - { - "id": "8e690c5f-8b0b-4163-a436-ff6b4d324737", - "outV": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "inV": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "uuid": "8e690c5f-8b0b-4163-a436-ff6b4d324737", - "label": "influences" - }, - { - "id": "3149fabe-41f4-4f5f-8d7c-514624094099", - "outV": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "inV": "98e3d193-d830-452f-9fe8-c21d258ef603", - "uuid": "3149fabe-41f4-4f5f-8d7c-514624094099", - "label": "influences" - }, - { - "id": "1293a923-8a8a-47c7-a08b-1f0f409dc858", - "outV": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "inV": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "uuid": "1293a923-8a8a-47c7-a08b-1f0f409dc858", - "label": "influences" - } - ] - } diff --git a/api/tests/v0/services/testdata/oil_wildcatter_problem.json b/api/tests/v0/services/testdata/oil_wildcatter_problem.json new file mode 100644 index 0000000..0f88c5d --- /dev/null +++ b/api/tests/v0/services/testdata/oil_wildcatter_problem.json @@ -0,0 +1,253 @@ +{ + "vertices": { + "project": { + "id": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "label": "project", + "sensitivity_label": "Open", + "name": "The Oil Wildcatter - v2", + "description": "The Oil Wildcatter" + }, + "objectives": [ + { + "id": "a725498b-b3bf-4350-b3ea-c980859fe36e", + "label": "objective", + "hierarchy": "Strategic", + "description": "Increase shareholder value", + "tag": [ + "Value" + ] + }, + { + "id": "4f489715-4a53-4d2e-9987-f82983dc083c", + "label": "objective", + "hierarchy": "Fundamental", + "description": "Increase Utility", + "tag": [ + "Value" + ] + } + ], + "opportunities": [], + "issues": [ + { + "id": "ea7e5e24-5ce3-49bd-9de2-b284dd43ae16", + "label": "issue", + "boundary": "in", + "description": "The utility will be measured by the NPV", + "shortname": "NPV", + "tag": [ + "Financial" + ], + "category": "Value Metric", + "probabilities": null + }, + { + "id": "b79acf5c-e437-44fa-987c-a540917bea97", + "label": "issue", + "boundary": "in", + "keyUncertainty": "true", + "description": "We are uncertain about if the reservoir is dry, wet or soaking", + "shortname": "Reservoir", + "tag": [ + "Reservoir" + ], + "category": "Uncertainty", + "probabilities": { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [ + [ + 0.7 + ], + [ + 0.2 + ], + [ + 0.1 + ] + ], + "variables": { + "variable": [ + "dry", + " wet", + " soaking" + ] + } + } + }, + { + "id": "2ce33e57-2484-4d2d-bc0d-fe1b6d6dec7c", + "label": "issue", + "boundary": "in", + "description": "We can conduct seismic acquisition", + "shortname": "Seismic", + "alternatives": [ + "Yes", + "No" + ], + "decisionType": "Focus", + "tag": [ + "Seismic" + ], + "category": "Decision", + "probabilities": null + }, + { + "id": "4c5c0d6f-d3fb-432a-9431-4144d56faa84", + "label": "issue", + "boundary": "in", + "keyUncertainty": "true", + "description": "The results of the seismic acquisition are uncertain and not\n known yet", + "shortname": "Seismic Result", + "tag": [ + "Seismic" + ], + "category": "Uncertainty", + "probabilities": { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [ + [ + 0.5 + ], + [ + 0.3 + ], + [ + 0.2 + ] + ], + "variables": { + "variable": [ + "dry", + " wet", + " soaking" + ] + } + } + }, + { + "id": "14ae2916-aa4f-4af3-adac-b5e8c895dea5", + "label": "issue", + "boundary": "in", + "description": "Should the reservoir be drilled or not?", + "shortname": "Drill?", + "alternatives": [ + "Yes", + "No" + ], + "decisionType": "Focus", + "tag": [ + "Drilling" + ], + "category": "Decision", + "probabilities": null + } + ], + "merged_issues": [] + }, + "edges": [ + { + "version": "v0", + "uuid": "0c4dbe58-3b30-4bc5-a1fc-e5cb1d31faf3", + "outV": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "inV": "ea7e5e24-5ce3-49bd-9de2-b284dd43ae16", + "id": "0c4dbe58-3b30-4bc5-a1fc-e5cb1d31faf3", + "label": "contains" + }, + { + "version": "v0", + "uuid": "ac8987cb-a9ef-4542-b3d9-d76370127667", + "outV": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "inV": "a725498b-b3bf-4350-b3ea-c980859fe36e", + "id": "ac8987cb-a9ef-4542-b3d9-d76370127667", + "label": "contains" + }, + { + "version": "v0", + "uuid": "3f6757d4-fcfe-417a-b406-6884adb19227", + "outV": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "inV": "4f489715-4a53-4d2e-9987-f82983dc083c", + "id": "3f6757d4-fcfe-417a-b406-6884adb19227", + "label": "contains" + }, + { + "version": "v0", + "uuid": "53ea6083-7d72-4748-aa9e-b0e89dc7dcac", + "outV": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "inV": "b79acf5c-e437-44fa-987c-a540917bea97", + "id": "53ea6083-7d72-4748-aa9e-b0e89dc7dcac", + "label": "contains" + }, + { + "version": "v0", + "uuid": "c7b07347-76be-405b-8f4b-de110d0bc165", + "outV": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "inV": "2ce33e57-2484-4d2d-bc0d-fe1b6d6dec7c", + "id": "c7b07347-76be-405b-8f4b-de110d0bc165", + "label": "contains" + }, + { + "version": "v0", + "uuid": "e2b5d662-e77a-4949-900b-ada21149b4b6", + "outV": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "inV": "4c5c0d6f-d3fb-432a-9431-4144d56faa84", + "id": "e2b5d662-e77a-4949-900b-ada21149b4b6", + "label": "contains" + }, + { + "version": "v0", + "uuid": "3abd2834-042c-4317-b966-b3a5f4f352f8", + "outV": "1bfcc6da-f610-4e07-ad2e-b331b862333d", + "inV": "14ae2916-aa4f-4af3-adac-b5e8c895dea5", + "id": "3abd2834-042c-4317-b966-b3a5f4f352f8", + "label": "contains" + }, + { + "version": "v0", + "uuid": "4ace5f1b-6d96-4c83-93f2-c7bd83dce43a", + "outV": "b79acf5c-e437-44fa-987c-a540917bea97", + "inV": "ea7e5e24-5ce3-49bd-9de2-b284dd43ae16", + "id": "4ace5f1b-6d96-4c83-93f2-c7bd83dce43a", + "label": "influences" + }, + { + "version": "v0", + "uuid": "4594f62b-a6a7-4719-8997-ad20265f27b0", + "outV": "b79acf5c-e437-44fa-987c-a540917bea97", + "inV": "4c5c0d6f-d3fb-432a-9431-4144d56faa84", + "id": "4594f62b-a6a7-4719-8997-ad20265f27b0", + "label": "influences" + }, + { + "version": "v0", + "uuid": "585c052d-9940-478a-a917-bd5b2ffe759f", + "outV": "2ce33e57-2484-4d2d-bc0d-fe1b6d6dec7c", + "inV": "4c5c0d6f-d3fb-432a-9431-4144d56faa84", + "id": "585c052d-9940-478a-a917-bd5b2ffe759f", + "label": "influences" + }, + { + "version": "v0", + "uuid": "45d31010-97b0-4126-8481-efa17fd795ad", + "outV": "2ce33e57-2484-4d2d-bc0d-fe1b6d6dec7c", + "inV": "ea7e5e24-5ce3-49bd-9de2-b284dd43ae16", + "id": "45d31010-97b0-4126-8481-efa17fd795ad", + "label": "influences" + }, + { + "version": "v0", + "uuid": "4493ba32-60c8-4703-a93a-041ccb18747b", + "outV": "4c5c0d6f-d3fb-432a-9431-4144d56faa84", + "inV": "14ae2916-aa4f-4af3-adac-b5e8c895dea5", + "id": "4493ba32-60c8-4703-a93a-041ccb18747b", + "label": "influences" + }, + { + "version": "v0", + "uuid": "dfe5c415-84e2-402a-8eff-9107ac76720f", + "outV": "14ae2916-aa4f-4af3-adac-b5e8c895dea5", + "inV": "ea7e5e24-5ce3-49bd-9de2-b284dd43ae16", + "id": "dfe5c415-84e2-402a-8eff-9107ac76720f", + "label": "influences" + } + ] +} diff --git a/api/tests/v0/services/testdata/simple_id.json b/api/tests/v0/services/testdata/simple_id.json new file mode 100644 index 0000000..8f55736 --- /dev/null +++ b/api/tests/v0/services/testdata/simple_id.json @@ -0,0 +1,169 @@ +{ + "vertices": { + "project": { + "id": "a1e0a9bb-4f83-4704-8e80-4359fe381e11", + "label": "project", + "name": "simple id" + }, + "objectives": [ + { + "id": "b674098b-36f4-4659-b38a-4239c89032c4", + "label": "objective", + "hierarchy": "Fundamental", + "description": "v", + "tag": [ + "v" + ] + } + ], + "opportunities": [ + { + "id": "a971770d-17f2-4b17-9ac2-06672783c6b4", + "label": "opportunity", + "description": "a", + "tag": [] + } + ], + "issues": [ + { + "id": "b1f3d18d-20a8-475b-981f-32449d1ee6bd", + "label": "issue", + "boundary": "in", + "comments": [ + { + "comment": "www", + "author": "User" + } + ], + "description": "b", + "shortname": "b", + "alternatives": [ + "y", + "n" + ], + "decisionType": "Focus", + "tag": [ + "b" + ], + "category": "Decision", + "probabilities": { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [ + [ + null + ] + ], + "variables": { + "variable": [ + "outcome" + ] + } + } + }, + { + "id": "c3d0dacc-990e-4557-8c5f-d33f9b3ede89", + "label": "issue", + "boundary": "in", + "keyUncertainty": "true", + "description": "a", + "shortname": "a", + "tag": [ + "a" + ], + "category": "Uncertainty", + "probabilities": null + }, + { + "id": "8106127e-ab4a-4aa5-a865-8b55789e7d7a", + "label": "issue", + "boundary": "in", + "description": "v", + "shortname": "v", + "tag": [ + "v" + ], + "category": "Value Metric", + "probabilities": { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [ + [ + null + ] + ], + "variables": { + "variable": [ + "outcome" + ] + } + } + } + ], + "merged_issues": [] + }, + "edges": [ + { + "version": "v0", + "uuid": "52ae073f-dae9-4427-aa94-567f033e3a24", + "outV": "a1e0a9bb-4f83-4704-8e80-4359fe381e11", + "inV": "b1f3d18d-20a8-475b-981f-32449d1ee6bd", + "id": "52ae073f-dae9-4427-aa94-567f033e3a24", + "label": "contains" + }, + { + "version": "v0", + "uuid": "3b1ae234-9c1b-478f-a664-4e2b944e33b1", + "outV": "a1e0a9bb-4f83-4704-8e80-4359fe381e11", + "inV": "b674098b-36f4-4659-b38a-4239c89032c4", + "id": "3b1ae234-9c1b-478f-a664-4e2b944e33b1", + "label": "contains" + }, + { + "version": "v0", + "uuid": "5170e558-83f2-46ca-877f-ed5073549393", + "outV": "a1e0a9bb-4f83-4704-8e80-4359fe381e11", + "inV": "c3d0dacc-990e-4557-8c5f-d33f9b3ede89", + "id": "5170e558-83f2-46ca-877f-ed5073549393", + "label": "contains" + }, + { + "version": "v0", + "uuid": "2c042b81-9c98-4730-a03c-2c908289982f", + "outV": "a1e0a9bb-4f83-4704-8e80-4359fe381e11", + "inV": "8106127e-ab4a-4aa5-a865-8b55789e7d7a", + "id": "2c042b81-9c98-4730-a03c-2c908289982f", + "label": "contains" + }, + { + "version": "v0", + "uuid": "47daef3f-c4ad-4aee-9fdc-2547661d44fb", + "outV": "a1e0a9bb-4f83-4704-8e80-4359fe381e11", + "inV": "a971770d-17f2-4b17-9ac2-06672783c6b4", + "id": "47daef3f-c4ad-4aee-9fdc-2547661d44fb", + "label": "contains" + }, + { + "version": "v0", + "uuid": "362a8e35-272f-4adb-a2ef-a21faa3fe1a1", + "outV": "b1f3d18d-20a8-475b-981f-32449d1ee6bd", + "inV": "8106127e-ab4a-4aa5-a865-8b55789e7d7a", + "id": "362a8e35-272f-4adb-a2ef-a21faa3fe1a1", + "label": "influences" + }, + { + "version": "v0", + "uuid": "9f4379ed-63d6-4a5f-8470-27cf6a575fc5", + "outV": "c3d0dacc-990e-4557-8c5f-d33f9b3ede89", + "inV": "b1f3d18d-20a8-475b-981f-32449d1ee6bd", + "id": "9f4379ed-63d6-4a5f-8470-27cf6a575fc5", + "label": "influences" + }, + { + "version": "v0", + "uuid": "77d09215-f210-40af-99db-f0ecdea81484", + "outV": "b674098b-36f4-4659-b38a-4239c89032c4", + "inV": "8106127e-ab4a-4aa5-a865-8b55789e7d7a", + "id": "77d09215-f210-40af-99db-f0ecdea81484", + "label": "has_value_metric" + } + ] +} diff --git a/api/tests/v0/services/testdata/used_car_buyer_model_response.json b/api/tests/v0/services/testdata/used_car_buyer_model_response.json deleted file mode 100644 index 608f90f..0000000 --- a/api/tests/v0/services/testdata/used_car_buyer_model_response.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "vertices": [ - { - "tag": ["State"], - "category": "Uncertainty", - "index": "0", - "description": "Joe does not know the state of the car", - "shortname": "State", - "keyUncertainty": "true", - "decisionType": "", - "alternatives": ["Peach","Lemon"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.5, - 0.5 - ], - [ - 0.4, - 0.6 - ] - ], - "variables": { - "Node1": [ - "Outcome1", - "Outcome2" - ], - "Node2": [ - "Outcome21", - "Outcome22" - ] - } - }, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "timestamp": "1712648453.1573343", - "date": "2024-04-09 07:40:53.157336", - "ids": "test", - "id": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "label": "issue" - }, - { - "tag": ["Test Result"], - "category": "Uncertainty", - "index": "0", - "description": "The result of the test is currently unknown", - "shortname": "Test Result", - "keyUncertainty": "true", - "decisionType": "", - "alternatives": ["no Test","Peach","Lemon"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.5, - 0.5 - ], - [ - 0.4, - 0.6 - ] - ], - "variables": { - "Node1": [ - "Outcome1", - "Outcome2" - ], - "Node2": [ - "Outcome21", - "Outcome22" - ] - } - }, - "influenceNodeUUID": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "boundary": "in", - "comments": null, - "uuid": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "timestamp": "1712648489.467934", - "date": "2024-04-09 07:41:29.467935", - "ids": "test", - "id": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "label": "issue" - }, - { - "tag": ["Buy"], - "category": "Decision", - "index": "0", - "description": "We can buy the car", - "shortname": "Buy", - "keyUncertainty": "false", - "decisionType": "Focus", - "alternatives": ["Buy with guarantee","Buy without guarantee","Do not buy"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.5, - 0.5 - ], - [ - 0.4, - 0.6 - ] - ], - "variables": { - "Node1": [ - "Outcome1", - "Outcome2" - ], - "Node2": [ - "Outcome21", - "Outcome22" - ] - } - }, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "98e3d193-d830-452f-9fe8-c21d258ef603", - "timestamp": "1712648530.228483", - "date": "2024-04-09 07:42:10.228485", - "ids": "test", - "id": "98e3d193-d830-452f-9fe8-c21d258ef603", - "label": "issue" - }, - { - "tag": ["Test"], - "category": "Decision", - "index": "0", - "description": "Joe can test the car", - "shortname": "Test", - "keyUncertainty": "false", - "decisionType": "Focus", - "alternatives": ["Test","no Test"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.5, - 0.5 - ], - [ - 0.4, - 0.6 - ] - ], - "variables": { - "Node1": [ - "Outcome1", - "Outcome2" - ], - "Node2": [ - "Outcome21", - "Outcome22" - ] - } - }, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "timestamp": "1712648468.41026", - "date": "2024-04-09 07:41:08.410262", - "ids": "test", - "id": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "label": "issue" - }, - { - "tag": ["Value"], - "category": "Value Metric", - "index": "0", - "description": "Value", - "shortname": "Value", - "keyUncertainty": "false", - "decisionType": "", - "alternatives": ["Test"], - "probabilities": { - "dtype": "DiscreteUnconditionalProbability", - "probability_function": [ - [ - 0.5, - 0.5 - ], - [ - 0.4, - 0.6 - ] - ], - "variables": { - "Node1": [ - "Outcome1", - "Outcome2" - ], - "Node2": [ - "Outcome21", - "Outcome22" - ] - } - }, - "influenceNodeUUID": "", - "boundary": "in", - "comments": null, - "uuid": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "timestamp": "1712648501.9647892", - "date": "2024-04-09 07:41:41.964793", - "ids": "test", - "id": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "label": "issue" - } - ], - "edges": [ - { - "id": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", - "outV": "98e3d193-d830-452f-9fe8-c21d258ef603", - "inV": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "uuid": "a6ab145e-2ca9-49e2-8c4f-9607688e57a9", - "label": "influences" - }, - { - "id": "44e4aed1-ec53-4d9c-9401-5204d913d7a5", - "outV": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "inV": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "uuid": "44e4aed1-ec53-4d9c-9401-5204d913d7a5", - "label": "influences" - }, - { - "id": "8e690c5f-8b0b-4163-a436-ff6b4d324737", - "outV": "51cd8e4f-aa04-48e2-8cdf-83a3c9ef978e", - "inV": "d52cadfd-c3b8-4531-8e3b-b7e966271edb", - "uuid": "8e690c5f-8b0b-4163-a436-ff6b4d324737", - "label": "influences" - }, - { - "id": "3149fabe-41f4-4f5f-8d7c-514624094099", - "outV": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "inV": "98e3d193-d830-452f-9fe8-c21d258ef603", - "uuid": "3149fabe-41f4-4f5f-8d7c-514624094099", - "label": "influences" - }, - { - "id": "1293a923-8a8a-47c7-a08b-1f0f409dc858", - "outV": "ad651f50-22de-4f85-a560-bf5fb2d9f706", - "inV": "72a8cf93-7400-4511-ae64-f72c2de2c16c", - "uuid": "1293a923-8a8a-47c7-a08b-1f0f409dc858", - "label": "influences" - } - ] -} diff --git a/api/tests/v0/services/testdata/used_car_buyer_problem.json b/api/tests/v0/services/testdata/used_car_buyer_problem.json new file mode 100644 index 0000000..ba96639 --- /dev/null +++ b/api/tests/v0/services/testdata/used_car_buyer_problem.json @@ -0,0 +1,286 @@ +{ + "vertices": { + "project": { + "id": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "label": "project", + "sensitivity_label": "Open", + "name": "The Used Car Buyer Problem", + "description": "The Used Car Buyer Problem" + }, + "objectives": [ + { + "id": "5e02460a-0f8e-463f-b80d-66056377d3a6", + "label": "objective", + "hierarchy": "Mean", + "description": "Increase wealth", + "index": "0", + "tag": [ + "Value" + ] + } + ], + "opportunities": [ + { + "id": "67d4da1d-da3e-4ebe-bf1d-c5c9b73c3441", + "label": "opportunity", + "description": "Joe can buy a car with a price of 1000 USD while the value is\n 1100 USD", + "index": "0", + "tag": [ + "subsurface" + ] + } + ], + "issues": [ + { + "id": "ab2b3630-46fe-425c-9a27-e91a5b74285d", + "label": "issue", + "boundary": "in", + "description": "Value", + "index": "0", + "shortname": "Value", + "alternatives": [ + "Test" + ], + "tag": [ + "Value" + ], + "category": "Value Metric" + }, + { + "id": "e5ade2d8-4033-4bef-a93a-094f2d1c3b2f", + "label": "issue", + "boundary": "in", + "keyUncertainty": "None", + "description": "Joe can test the car", + "index": "0", + "shortname": "Test", + "alternatives": [ + "Test", + " no Test" + ], + "decisionType": "Focus", + "tag": [ + "Test" + ], + "category": "Decision" + }, + { + "id": "5077b271-7c20-412c-808d-6e8072e071bd", + "label": "issue", + "boundary": "in", + "keyUncertainty": "None", + "description": "We can buy the car", + "index": "0", + "shortname": "Buy", + "alternatives": [ + "Buy with guarantee", + " Buy without guarantee", + " Do not buy" + ], + "decisionType": "Focus", + "tag": [ + "Buy" + ], + "category": "Decision" + }, + { + "id": "a9283f74-8569-480d-9dfd-b4be2eea4007", + "label": "issue", + "boundary": "in", + "keyUncertainty": "true", + "description": "Joe does not know the state of the car", + "index": "0", + "shortname": "State", + "alternatives": [ + "Peach", + "Lemon" + ], + "tag": [ + "State" + ], + "category": "Uncertainty", + "probabilities": { + "dtype": "DiscreteUnconditionalProbability", + "probability_function": [ + [ + 0.8 + ], + [ + 0.2 + ] + ], + "variables": { + "Node1": [ + "Peach", + "Lemon" + ] + } + } + }, + { + "id": "8a935e05-ecf6-41e1-8b85-e6b11086f49b", + "label": "issue", + "boundary": "in", + "keyUncertainty": "true", + "description": "The result of the test is currently unknown", + "index": "0", + "shortname": "Test Result", + "influenceNodeUUID": "ad651f50-22de-4f85-a560-bf5fb2d9f706", + "alternatives": [ + "no Test", + "Peach", + "Lemon" + ], + "tag": [ + "Test Result" + ], + "category": "Uncertainty", + "probabilities": { + "dtype": "DiscreteConditionalProbability", + "probability_function": [ + [ + 0.0, + 0.0, + 1.0, + 1.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 1.0, + 0.0, + 0.0 + ] + ], + "variables": { + "Test Result": [ + "no Test", + "Peach", + "Lemon" + ], + "Test": [ + "yes", + "no" + ], + "State": [ + "Peach", + "Lemon" + ] + } + } + } + ], + "merged_issues": [] + }, + "edges": [ + { + "version": "v0", + "uuid": "84552b54-e17d-4957-89ea-b9bdf6e3b11d", + "outV": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "inV": "ab2b3630-46fe-425c-9a27-e91a5b74285d", + "id": "84552b54-e17d-4957-89ea-b9bdf6e3b11d", + "label": "contains" + }, + { + "version": "v0", + "uuid": "8d87da6e-73f6-46d6-9020-9806d8c20557", + "outV": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "inV": "67d4da1d-da3e-4ebe-bf1d-c5c9b73c3441", + "id": "8d87da6e-73f6-46d6-9020-9806d8c20557", + "label": "contains" + }, + { + "version": "v0", + "uuid": "8946ecd7-b26b-4a1c-b6e3-13f4306d345e", + "outV": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "inV": "e5ade2d8-4033-4bef-a93a-094f2d1c3b2f", + "id": "8946ecd7-b26b-4a1c-b6e3-13f4306d345e", + "label": "contains" + }, + { + "version": "v0", + "uuid": "ba347042-c89d-4fbb-923e-d4931fc964a3", + "outV": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "inV": "5e02460a-0f8e-463f-b80d-66056377d3a6", + "id": "ba347042-c89d-4fbb-923e-d4931fc964a3", + "label": "contains" + }, + { + "version": "v0", + "uuid": "5962c776-8536-4c9e-87d5-c3e119422d31", + "outV": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "inV": "5077b271-7c20-412c-808d-6e8072e071bd", + "id": "5962c776-8536-4c9e-87d5-c3e119422d31", + "label": "contains" + }, + { + "version": "v0", + "uuid": "be7b59b6-4d3e-470c-a706-83ca1928ec66", + "outV": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "inV": "a9283f74-8569-480d-9dfd-b4be2eea4007", + "id": "be7b59b6-4d3e-470c-a706-83ca1928ec66", + "label": "contains" + }, + { + "version": "v0", + "uuid": "379b29b2-ddc0-44ab-984b-71d1a05f0533", + "outV": "72ba27a5-2e9c-4551-aa1a-6ad9d676d67b", + "inV": "8a935e05-ecf6-41e1-8b85-e6b11086f49b", + "id": "379b29b2-ddc0-44ab-984b-71d1a05f0533", + "label": "contains" + }, + { + "version": "v0", + "uuid": "36a6987d-ef18-405d-a9ff-ce44f9b30a7e", + "outV": "e5ade2d8-4033-4bef-a93a-094f2d1c3b2f", + "inV": "8a935e05-ecf6-41e1-8b85-e6b11086f49b", + "id": "36a6987d-ef18-405d-a9ff-ce44f9b30a7e", + "label": "influences" + }, + { + "version": "v0", + "uuid": "28276cd6-48f9-4e07-982f-15ea114cfc0f", + "outV": "e5ade2d8-4033-4bef-a93a-094f2d1c3b2f", + "inV": "ab2b3630-46fe-425c-9a27-e91a5b74285d", + "id": "28276cd6-48f9-4e07-982f-15ea114cfc0f", + "label": "influences" + }, + { + "version": "v0", + "uuid": "4c1d56d6-0d27-40c7-b836-590cb4b69c88", + "outV": "5077b271-7c20-412c-808d-6e8072e071bd", + "inV": "ab2b3630-46fe-425c-9a27-e91a5b74285d", + "id": "4c1d56d6-0d27-40c7-b836-590cb4b69c88", + "label": "influences" + }, + { + "version": "v0", + "uuid": "3ce899ad-c452-436c-934f-380f93087225", + "outV": "a9283f74-8569-480d-9dfd-b4be2eea4007", + "inV": "8a935e05-ecf6-41e1-8b85-e6b11086f49b", + "id": "3ce899ad-c452-436c-934f-380f93087225", + "label": "influences" + }, + { + "version": "v0", + "uuid": "276fdf2b-236a-42bd-8b2e-c7fcc652c855", + "outV": "a9283f74-8569-480d-9dfd-b4be2eea4007", + "inV": "ab2b3630-46fe-425c-9a27-e91a5b74285d", + "id": "276fdf2b-236a-42bd-8b2e-c7fcc652c855", + "label": "influences" + }, + { + "version": "v0", + "uuid": "79240580-cd65-43ec-98bd-5689a16e5044", + "outV": "8a935e05-ecf6-41e1-8b85-e6b11086f49b", + "inV": "5077b271-7c20-412c-808d-6e8072e071bd", + "id": "79240580-cd65-43ec-98bd-5689a16e5044", + "label": "influences" + } + ] +} diff --git a/test.bifxml b/test.bifxml new file mode 100644 index 0000000..81e50cb --- /dev/null +++ b/test.bifxml @@ -0,0 +1,67 @@ + + + + + + + + Value + Value + 0 + + + + Test + Joe can test the car + Test + no Test + + + + Buy + We can buy the car + Buy with guarantee + Buy without guarantee + Do not buy + + + + State + Joe does not know the state of the car + Peach + Lemon + + + + Test Result + The result of the test is currently unknown + no Test + Peach + Lemon + + + + + Value + State + Buy + Test + 0 0 0 0 0 0 0 0 0 0 0 0
+
+ + Buy + Test Result + + + State + 0.8 0.2
+
+ + Test Result + State + Test + 0 1 0 1 0 0 0 0 1 1 0 0
+
+ +
+
diff --git a/test2.png b/test2.png new file mode 100644 index 0000000..197086e Binary files /dev/null and b/test2.png differ diff --git a/web/src/features/visualization/cid/CIDGraph.jsx b/web/src/features/visualization/cid/CIDGraph.jsx index f24c0a6..70f08b9 100644 --- a/web/src/features/visualization/cid/CIDGraph.jsx +++ b/web/src/features/visualization/cid/CIDGraph.jsx @@ -210,6 +210,8 @@ class CIDGraph extends abstractGraph { static MakeGraphNodes(obj) { // create an array with nodes //const groups = CIDGraph.formatNode(obj); + console.log("MakeGraphNode", Object.keys(obj)); + console.log("MakeGraphNode", Object.keys(obj.vertices)); let categories = Object.keys(obj.vertices); let nodes = []; for (let category of categories) {