diff --git a/poetry.lock b/poetry.lock index 9321211..5bec7cd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.16" description = "A light, configurable Sphinx theme" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -12,11 +11,21 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "appnope" version = "0.1.4" description = "Disable App Nap on macOS >= 10.9" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -24,11 +33,21 @@ files = [ {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, ] +[[package]] +name = "apted" +version = "1.0.3" +description = "APTED algorithm for the Tree Edit Distance" +optional = false +python-versions = "*" +files = [ + {file = "apted-1.0.3-py3-none-any.whl", hash = "sha256:74193369d023649d335269e67c4df07f922959e5ac2597de1b79af4e694150e8"}, + {file = "apted-1.0.3.tar.gz", hash = "sha256:befa5181e2d4457fa88e54995a82604ee048bb2fbc781ea97d8e1856b4715ce9"}, +] + [[package]] name = "astroid" version = "2.15.8" description = "An abstract syntax tree for Python with inference support." -category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -45,7 +64,6 @@ wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -56,7 +74,6 @@ files = [ name = "attrs" version = "24.2.0" description = "Classes Without Boilerplate" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -76,7 +93,6 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] name = "babel" version = "2.16.0" description = "Internationalization utilities" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -91,7 +107,6 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "dev" optional = false python-versions = "*" files = [ @@ -103,7 +118,6 @@ files = [ name = "black" version = "22.12.0" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -137,18 +151,17 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "boto3" -version = "1.34.159" +version = "1.35.34" description = "The AWS SDK for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.159-py3-none-any.whl", hash = "sha256:21120d23cc37c0e80dc4f64434bc5664d2a5645dcd9bf8a8fa97ed5c82164ca0"}, - {file = "boto3-1.34.159.tar.gz", hash = "sha256:ffe7bbb88ba81b5d54bc8fa0cfb2f3b7fe63a6cffa0f9207df2ef5c22a1c0587"}, + {file = "boto3-1.35.34-py3-none-any.whl", hash = "sha256:291e7b97a34967ed93297e6171f1bebb8529e64633dd48426760e3fdef1cdea8"}, + {file = "boto3-1.35.34.tar.gz", hash = "sha256:57e6ee8504e7929bc094bb2afc879943906064179a1e88c23b4812e2c6f61532"}, ] [package.dependencies] -botocore = ">=1.34.159,<1.35.0" +botocore = ">=1.35.34,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -157,14 +170,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.159" +version = "1.35.34" description = "Low-level, data-driven core of boto 3." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.159-py3-none-any.whl", hash = "sha256:7633062491457419a49f5860c014251ae85689f78266a3ce020c2c8688a76b97"}, - {file = "botocore-1.34.159.tar.gz", hash = "sha256:dc28806eb21e3c8d690c422530dff8b4b242ac033cbe98f160a9d37796c09cb1"}, + {file = "botocore-1.35.34-py3-none-any.whl", hash = "sha256:ccb0fe397b11b81c9abc0c87029d17298e17bf658d8db5c0c5a551a12a207e7a"}, + {file = "botocore-1.35.34.tar.gz", hash = "sha256:789b6501a3bb4a9591c1fe10da200cc315c1fa5df5ada19c720d8ef06439b3e3"}, ] [package.dependencies] @@ -176,13 +188,12 @@ urllib3 = [ ] [package.extras] -crt = ["awscrt (==0.21.2)"] +crt = ["awscrt (==0.22.0)"] [[package]] name = "brotli" version = "1.1.0" description = "Python bindings for the Brotli compression library" -category = "main" optional = false python-versions = "*" files = [ @@ -275,7 +286,6 @@ files = [ name = "brotlicffi" version = "1.1.0.0" description = "Python CFFI bindings to the Brotli library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -315,7 +325,6 @@ cffi = ">=1.0.0" name = "cachedmethods" version = "0.1.4" description = "" -category = "main" optional = false python-versions = ">=3.6.0" files = [ @@ -324,91 +333,89 @@ files = [ [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" -version = "1.17.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, - {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, - {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, - {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, - {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, - {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, - {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, - {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, - {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, - {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, - {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, - {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, - {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, - {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, - {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, - {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, - {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, - {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, - {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, - {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, - {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, - {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, - {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, - {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, - {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, - {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, - {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, - {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, - {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, - {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, - {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -418,7 +425,6 @@ pycparser = "*" name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -430,7 +436,6 @@ files = [ name = "cgrtools" version = "4.1.35" description = "" -category = "main" optional = false python-versions = ">=3.6.1" files = [ @@ -457,7 +462,6 @@ mrv = ["lxml (>=4.1)"] name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -555,14 +559,13 @@ files = [ [[package]] name = "cli-exit-tools" -version = "1.2.6" +version = "1.2.7" description = "functions to exit an cli application properly" -category = "main" optional = false python-versions = ">=3.8.0" files = [ - {file = "cli_exit_tools-1.2.6-py3-none-any.whl", hash = "sha256:b230a552266ec0e48a51da861fc03bd3a943458230551d8ab635256e797de746"}, - {file = "cli_exit_tools-1.2.6.tar.gz", hash = "sha256:e76d4b628f8a5bb6dbbfba8fb62d6124793960dba252e7e7a27897065fc487e1"}, + {file = "cli_exit_tools-1.2.7-py3-none-any.whl", hash = "sha256:bdfdd8b0613e49faf6f4d8695328ce815858f980ea8ab2ddb69867ca1970e551"}, + {file = "cli_exit_tools-1.2.7.tar.gz", hash = "sha256:e752427a4aa9db1f18370c8dc11ebef6e245cc5891ec2fa79e7169be583c2423"}, ] [package.dependencies] @@ -576,7 +579,6 @@ test = ["black", "codecov", "coloredlogs", "coverage", "flake8", "mypy", "pytest name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -591,7 +593,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "cloudpickle" version = "3.0.0" description = "Pickler class to extend the standard pickle.Pickler functionality" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -603,7 +604,6 @@ files = [ name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -615,7 +615,6 @@ files = [ name = "coverage" version = "7.6.1" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -703,7 +702,6 @@ toml = ["tomli"] name = "dask" version = "2024.2.1" description = "Parallel PyData with Task Scheduling" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -735,7 +733,6 @@ test = ["pandas[test]", "pre-commit", "pytest", "pytest-cov", "pytest-rerunfailu name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -747,7 +744,6 @@ files = [ name = "deprecated" version = "1.2.14" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -763,14 +759,13 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -781,7 +776,6 @@ profile = ["gprof2dot (>=2022.7.29)"] name = "distlib" version = "0.3.8" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" files = [ @@ -793,7 +787,6 @@ files = [ name = "docutils" version = "0.21.2" description = "Docutils -- Python Documentation Utilities" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -803,31 +796,29 @@ files = [ [[package]] name = "filelock" -version = "3.15.4" +version = "3.16.1" description = "A platform independent file lock." -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, + {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, + {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "fsspec" -version = "2024.6.1" +version = "2024.9.0" description = "File-system specification" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "fsspec-2024.6.1-py3-none-any.whl", hash = "sha256:3cb443f8bcd2efb31295a5b9fdb02aee81d8452c80d28f97a6d0959e6cee101e"}, - {file = "fsspec-2024.6.1.tar.gz", hash = "sha256:fad7d7e209dd4c1208e3bbfda706620e0da5142bebbd9c384afb95b07e798e49"}, + {file = "fsspec-2024.9.0-py3-none-any.whl", hash = "sha256:a0947d552d8a6efa72cc2c730b12c41d043509156966cca4fb157b0f2a0c574b"}, + {file = "fsspec-2024.9.0.tar.gz", hash = "sha256:4b0afb90c2f21832df142f292649035d80b421f60a9e1c027802e5a0da2b04e8"}, ] [package.extras] @@ -860,14 +851,13 @@ tqdm = ["tqdm"] [[package]] name = "identify" -version = "2.6.0" +version = "2.6.1" description = "File identification library for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, + {file = "identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0"}, + {file = "identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98"}, ] [package.extras] @@ -875,21 +865,22 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -899,29 +890,31 @@ files = [ [[package]] name = "importlib-metadata" -version = "8.2.0" +version = "8.5.0" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, - {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] [package.dependencies] -zipp = ">=0.5" +zipp = ">=3.20" [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -933,7 +926,6 @@ files = [ name = "invoke" version = "1.7.3" description = "Pythonic task execution" -category = "dev" optional = false python-versions = "*" files = [ @@ -945,7 +937,6 @@ files = [ name = "ipython" version = "7.34.0" description = "IPython: Productive Interactive Computing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -982,7 +973,6 @@ test = ["ipykernel", "nbformat", "nose (>=0.10.1)", "numpy (>=1.17)", "pygments" name = "isort" version = "5.13.2" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -997,7 +987,6 @@ colors = ["colorama (>=0.4.6)"] name = "jedi" version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1017,7 +1006,6 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1035,7 +1023,6 @@ i18n = ["Babel (>=2.7)"] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1047,7 +1034,6 @@ files = [ name = "lazy-object-proxy" version = "1.10.0" description = "A fast and thorough lazy object proxy." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1094,7 +1080,6 @@ files = [ name = "lib-detect-testenv" version = "2.0.8" description = "detects if pytest or doctest or pyrunner on pycharm is running" -category = "main" optional = false python-versions = ">=3.8.0" files = [ @@ -1109,7 +1094,6 @@ test = ["black", "codecov", "coloredlogs", "coverage", "flake8", "mypy", "pytest name = "locket" version = "1.0.0" description = "File-based locks for Python on Linux and Windows" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1121,7 +1105,6 @@ files = [ name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1191,7 +1174,6 @@ files = [ name = "matplotlib-inline" version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1206,7 +1188,6 @@ traitlets = "*" name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1216,14 +1197,13 @@ files = [ [[package]] name = "metaflow" -version = "2.12.11" +version = "2.12.24" description = "Metaflow: More Data Science, Less Engineering" -category = "main" optional = false python-versions = "*" files = [ - {file = "metaflow-2.12.11-py2.py3-none-any.whl", hash = "sha256:fec39343c8794cc9f3aef7998d1a1eb8cfab0b0d628173785a49f6a22c22816f"}, - {file = "metaflow-2.12.11.tar.gz", hash = "sha256:9b2d3572f992cd08c5c8a893821d9d78c9a5490f17df7837a37ff5ba814fa118"}, + {file = "metaflow-2.12.24-py2.py3-none-any.whl", hash = "sha256:37c55afc6cfb6a9f842767ff96e198b8ef83d838164188560d64591a3d453e0a"}, + {file = "metaflow-2.12.24.tar.gz", hash = "sha256:25d4af40361d762c367813a6b3dbd81a51a77b6845b14dea60a2b6ae99f35ba4"}, ] [package.dependencies] @@ -1231,38 +1211,40 @@ boto3 = "*" requests = "*" [package.extras] -stubs = ["metaflow-stubs (==2.12.11)"] +stubs = ["metaflow-stubs (==2.12.24)"] [[package]] name = "multiprocess" -version = "0.70.16" +version = "0.70.17" description = "better multiprocessing and multithreading in Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "multiprocess-0.70.16-pp310-pypy310_pp73-macosx_10_13_x86_64.whl", hash = "sha256:476887be10e2f59ff183c006af746cb6f1fd0eadcfd4ef49e605cbe2659920ee"}, - {file = "multiprocess-0.70.16-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d951bed82c8f73929ac82c61f01a7b5ce8f3e5ef40f5b52553b4f547ce2b08ec"}, - {file = "multiprocess-0.70.16-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37b55f71c07e2d741374998c043b9520b626a8dddc8b3129222ca4f1a06ef67a"}, - {file = "multiprocess-0.70.16-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba8c31889abf4511c7308a8c52bb4a30b9d590e7f58523302ba00237702ca054"}, - {file = "multiprocess-0.70.16-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:0dfd078c306e08d46d7a8d06fb120313d87aa43af60d66da43ffff40b44d2f41"}, - {file = "multiprocess-0.70.16-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e7b9d0f307cd9bd50851afaac0dba2cb6c44449efff697df7c7645f7d3f2be3a"}, - {file = "multiprocess-0.70.16-py310-none-any.whl", hash = "sha256:c4a9944c67bd49f823687463660a2d6daae94c289adff97e0f9d696ba6371d02"}, - {file = "multiprocess-0.70.16-py311-none-any.whl", hash = "sha256:af4cabb0dac72abfb1e794fa7855c325fd2b55a10a44628a3c1ad3311c04127a"}, - {file = "multiprocess-0.70.16-py312-none-any.whl", hash = "sha256:fc0544c531920dde3b00c29863377f87e1632601092ea2daca74e4beb40faa2e"}, - {file = "multiprocess-0.70.16-py38-none-any.whl", hash = "sha256:a71d82033454891091a226dfc319d0cfa8019a4e888ef9ca910372a446de4435"}, - {file = "multiprocess-0.70.16-py39-none-any.whl", hash = "sha256:a0bafd3ae1b732eac64be2e72038231c1ba97724b60b09400d68f229fcc2fbf3"}, - {file = "multiprocess-0.70.16.tar.gz", hash = "sha256:161af703d4652a0e1410be6abccecde4a7ddffd19341be0a7011b94aeb171ac1"}, + {file = "multiprocess-0.70.17-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ddb24e5bcdb64e90ec5543a1f05a39463068b6d3b804aa3f2a4e16ec28562d6"}, + {file = "multiprocess-0.70.17-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d729f55198a3579f6879766a6d9b72b42d4b320c0dcb7844afb774d75b573c62"}, + {file = "multiprocess-0.70.17-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2c82d0375baed8d8dd0d8c38eb87c5ae9c471f8e384ad203a36f095ee860f67"}, + {file = "multiprocess-0.70.17-pp38-pypy38_pp73-macosx_10_9_arm64.whl", hash = "sha256:a22a6b1a482b80eab53078418bb0f7025e4f7d93cc8e1f36481477a023884861"}, + {file = "multiprocess-0.70.17-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:349525099a0c9ac5936f0488b5ee73199098dac3ac899d81d326d238f9fd3ccd"}, + {file = "multiprocess-0.70.17-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:27b8409c02b5dd89d336107c101dfbd1530a2cd4fd425fc27dcb7adb6e0b47bf"}, + {file = "multiprocess-0.70.17-pp39-pypy39_pp73-macosx_10_13_arm64.whl", hash = "sha256:2ea0939b0f4760a16a548942c65c76ff5afd81fbf1083c56ae75e21faf92e426"}, + {file = "multiprocess-0.70.17-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:2b12e081df87ab755190e227341b2c3b17ee6587e9c82fecddcbe6aa812cd7f7"}, + {file = "multiprocess-0.70.17-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a0f01cd9d079af7a8296f521dc03859d1a414d14c1e2b6e676ef789333421c95"}, + {file = "multiprocess-0.70.17-py310-none-any.whl", hash = "sha256:38357ca266b51a2e22841b755d9a91e4bb7b937979a54d411677111716c32744"}, + {file = "multiprocess-0.70.17-py311-none-any.whl", hash = "sha256:2884701445d0177aec5bd5f6ee0df296773e4fb65b11903b94c613fb46cfb7d1"}, + {file = "multiprocess-0.70.17-py312-none-any.whl", hash = "sha256:2818af14c52446b9617d1b0755fa70ca2f77c28b25ed97bdaa2c69a22c47b46c"}, + {file = "multiprocess-0.70.17-py313-none-any.whl", hash = "sha256:20c28ca19079a6c879258103a6d60b94d4ffe2d9da07dda93fb1c8bc6243f522"}, + {file = "multiprocess-0.70.17-py38-none-any.whl", hash = "sha256:1d52f068357acd1e5bbc670b273ef8f81d57863235d9fbf9314751886e141968"}, + {file = "multiprocess-0.70.17-py39-none-any.whl", hash = "sha256:c3feb874ba574fbccfb335980020c1ac631fbf2a3f7bee4e2042ede62558a021"}, + {file = "multiprocess-0.70.17.tar.gz", hash = "sha256:4ae2f11a3416809ebc9a48abfc8b14ecce0652a0944731a1493a3c1ba44ff57a"}, ] [package.dependencies] -dill = ">=0.3.8" +dill = ">=0.3.9" [[package]] name = "multivolumefile" version = "0.2.3" description = "multi volume file wrapper library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1279,7 +1261,6 @@ type = ["mypy", "mypy-extensions"] name = "mypy" version = "0.800" description = "Optional static typing for Python" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1319,7 +1300,6 @@ dmypy = ["psutil (>=4.0)"] name = "mypy-extensions" version = "0.4.4" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" optional = false python-versions = ">=2.7" files = [ @@ -1330,7 +1310,6 @@ files = [ name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -1342,7 +1321,6 @@ files = [ name = "numpy" version = "1.26.4" description = "Fundamental package for array computing in Python" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -1388,7 +1366,6 @@ files = [ name = "packaging" version = "24.1" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1400,7 +1377,6 @@ files = [ name = "pandas" version = "1.5.3" description = "Powerful data structures for data analysis, time series, and statistics" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1448,7 +1424,6 @@ test = ["hypothesis (>=5.5.3)", "pytest (>=6.0)", "pytest-xdist (>=1.31)"] name = "parso" version = "0.8.4" description = "A Python Parser" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1464,7 +1439,6 @@ testing = ["docopt", "pytest"] name = "partd" version = "1.4.2" description = "Appendable key-value storage" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -1483,7 +1457,6 @@ complete = ["blosc", "numpy (>=1.20.0)", "pandas (>=1.3)", "pyzmq"] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1495,7 +1468,6 @@ files = [ name = "pexpect" version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." -category = "dev" optional = false python-versions = "*" files = [ @@ -1510,7 +1482,6 @@ ptyprocess = ">=0.5" name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" optional = false python-versions = "*" files = [ @@ -1522,7 +1493,6 @@ files = [ name = "pillow" version = "10.4.0" description = "Python Imaging Library (Fork)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1618,26 +1588,24 @@ xmp = ["defusedxml"] [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1653,7 +1621,6 @@ testing = ["pytest", "pytest-benchmark"] name = "pre-commit" version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1670,14 +1637,13 @@ virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.48" description = "Library for building powerful interactive command lines in Python" -category = "dev" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, ] [package.dependencies] @@ -1687,7 +1653,6 @@ wcwidth = "*" name = "psutil" version = "6.0.0" description = "Cross-platform lib for process and system monitoring in Python." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ @@ -1717,7 +1682,6 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -1729,7 +1693,6 @@ files = [ name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1741,7 +1704,6 @@ files = [ name = "py7zr" version = "0.18.12" description = "Pure python 7-zip library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1772,7 +1734,6 @@ test-compat = ["libarchive-c"] name = "pybcj" version = "1.0.2" description = "bcj filter library" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1827,7 +1788,6 @@ test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-cov"] name = "pycparser" version = "2.22" description = "C parser in Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1837,51 +1797,170 @@ files = [ [[package]] name = "pycryptodomex" -version = "3.20.0" +version = "3.21.0" description = "Cryptographic library for Python" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pycryptodomex-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dbeb84a399373df84a69e0919c1d733b89e049752426041deeb30d68e9867822"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a192fb46c95489beba9c3f002ed7d93979423d1b2a53eab8771dbb1339eb3ddd"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1233443f19d278c72c4daae749872a4af3787a813e05c3561c73ab0c153c7b0f"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbb07f88e277162b8bfca7134b34f18b400d84eac7375ce73117f865e3c80d4c"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:e859e53d983b7fe18cb8f1b0e29d991a5c93be2c8dd25db7db1fe3bd3617f6f9"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:ef046b2e6c425647971b51424f0f88d8a2e0a2a63d3531817968c42078895c00"}, + {file = "pycryptodomex-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:da76ebf6650323eae7236b54b1b1f0e57c16483be6e3c1ebf901d4ada47563b6"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c07e64867a54f7e93186a55bec08a18b7302e7bee1b02fd84c6089ec215e723a"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:56435c7124dd0ce0c8bdd99c52e5d183a0ca7fdcd06c5d5509423843f487dd0b"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65d275e3f866cf6fe891411be9c1454fb58809ccc5de6d3770654c47197acd65"}, + {file = "pycryptodomex-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5241bdb53bcf32a9568770a6584774b1b8109342bd033398e4ff2da052123832"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:34325b84c8b380675fd2320d0649cdcbc9cf1e0d1526edbe8fce43ed858cdc7e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:103c133d6cd832ae7266feb0a65b69e3a5e4dbbd6f3a3ae3211a557fd653f516"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77ac2ea80bcb4b4e1c6a596734c775a1615d23e31794967416afc14852a639d3"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9aa0cf13a1a1128b3e964dc667e5fe5c6235f7d7cfb0277213f0e2a783837cc2"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46eb1f0c8d309da63a2064c28de54e5e614ad17b7e2f88df0faef58ce192fc7b"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:cc7e111e66c274b0df5f4efa679eb31e23c7545d702333dfd2df10ab02c2a2ce"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:770d630a5c46605ec83393feaa73a9635a60e55b112e1fb0c3cea84c2897aa0a"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:52e23a0a6e61691134aa8c8beba89de420602541afaae70f66e16060fdcd677e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win32.whl", hash = "sha256:a3d77919e6ff56d89aada1bd009b727b874d464cb0e2e3f00a49f7d2e709d76e"}, + {file = "pycryptodomex-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b0e9765f93fe4890f39875e6c90c96cb341767833cfa767f41b490b506fa9ec0"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:feaecdce4e5c0045e7a287de0c4351284391fe170729aa9182f6bd967631b3a8"}, + {file = "pycryptodomex-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:365aa5a66d52fd1f9e0530ea97f392c48c409c2f01ff8b9a39c73ed6f527d36c"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3efddfc50ac0ca143364042324046800c126a1d63816d532f2e19e6f2d8c0c31"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df2608682db8279a9ebbaf05a72f62a321433522ed0e499bc486a6889b96bf3"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5823d03e904ea3e53aebd6799d6b8ec63b7675b5d2f4a4bd5e3adcb512d03b37"}, + {file = "pycryptodomex-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:27e84eeff24250ffec32722334749ac2a57a5fd60332cd6a0680090e7c42877e"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ef436cdeea794015263853311f84c1ff0341b98fc7908e8a70595a68cefd971"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a1058e6dfe827f4209c5cae466e67610bcd0d66f2f037465daa2a29d92d952b"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ba09a5b407cbb3bcb325221e346a140605714b5e880741dc9a1e9ecf1688d42"}, + {file = "pycryptodomex-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8a9d8342cf22b74a746e3c6c9453cb0cfbb55943410e3a2619bd9164b48dc9d9"}, + {file = "pycryptodomex-3.21.0.tar.gz", hash = "sha256:222d0bd05381dd25c32dd6065c071ebf084212ab79bab4599ba9e6a3e0009e6c"}, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" files = [ - {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, - {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, ] +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = {version = ">=4.6.1", markers = "python_version < \"3.13\""} + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1896,7 +1975,6 @@ windows-terminal = ["colorama (>=0.4.6)"] name = "pylint" version = "2.17.7" description = "python code static checker" -category = "dev" optional = false python-versions = ">=3.7.2" files = [ @@ -1923,7 +2001,6 @@ testutils = ["gitpython (>3)"] name = "pyppmd" version = "0.18.3" description = "PPMd compression/decompression library" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2015,7 +2092,6 @@ test = ["coverage[toml] (>=5.2)", "hypothesis", "pytest (>=6.0)", "pytest-benchm name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2040,7 +2116,6 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm name = "pytest-black" version = "0.3.12" description = "A pytest plugin to enable format checking with black" -category = "dev" optional = false python-versions = ">=2.7" files = [ @@ -2056,7 +2131,6 @@ toml = "*" name = "pytest-cov" version = "3.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2075,7 +2149,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-datadir" version = "1.5.0" description = "pytest plugin for test data directories and files" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2090,7 +2163,6 @@ pytest = ">=5.0" name = "pytest-mccabe" version = "2.0" description = "pytest plugin to run the mccabe code complexity checker." -category = "dev" optional = false python-versions = "*" files = [ @@ -2106,7 +2178,6 @@ pytest = ">=5.4.0" name = "pytest-mock" version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2124,7 +2195,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2137,21 +2207,19 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2214,7 +2282,6 @@ files = [ name = "pyzstd" version = "0.16.1" description = "Python bindings to Zstandard (zstd) compression library." -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2313,7 +2380,6 @@ files = [ name = "rdchiral" version = "1.1.0" description = "Wrapper for RDKit's RunReactants to improve stereochemistry handling" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -2325,7 +2391,6 @@ files = [ name = "rdkit" version = "2023.9.6" description = "A collection of chemoinformatics and machine-learning software written in C++ and Python" -category = "main" optional = false python-versions = "*" files = [ @@ -2364,7 +2429,6 @@ Pillow = "*" name = "requests" version = "2.32.3" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2386,7 +2450,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "s3transfer" version = "0.10.2" description = "An Amazon S3 Transfer Manager" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2404,7 +2467,6 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] name = "scipy" version = "1.13.1" description = "Fundamental algorithms for scientific computing in Python" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2445,26 +2507,28 @@ test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "po [[package]] name = "setuptools" -version = "72.1.0" +version = "75.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-72.1.0-py3-none-any.whl", hash = "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1"}, - {file = "setuptools-72.1.0.tar.gz", hash = "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"}, + {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, + {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, ] [package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (>=1.11.0,<1.12.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2476,7 +2540,6 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" optional = false python-versions = "*" files = [ @@ -2488,7 +2551,6 @@ files = [ name = "sphinx" version = "7.4.7" description = "Python documentation generator" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -2525,7 +2587,6 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools name = "sphinxcontrib-applehelp" version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -2542,7 +2603,6 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -2559,7 +2619,6 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -2576,7 +2635,6 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -2591,7 +2649,6 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -2608,7 +2665,6 @@ test = ["defusedxml (>=0.7.1)", "pytest"] name = "sphinxcontrib-serializinghtml" version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -2625,7 +2681,6 @@ test = ["pytest"] name = "swifter" version = "1.4.0" description = "A package which efficiently applies any function to a pandas dataframe or series in the fastest available manner" -category = "main" optional = false python-versions = "*" files = [ @@ -2646,7 +2701,6 @@ notebook = ["ipywidgets (>=7.0.0)"] name = "texttable" version = "1.7.0" description = "module to create simple ASCII tables" -category = "main" optional = false python-versions = "*" files = [ @@ -2658,7 +2712,6 @@ files = [ name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2668,45 +2721,41 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" -category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] name = "toolz" -version = "0.12.1" +version = "1.0.0" description = "List processing tools and functional utilities" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "toolz-0.12.1-py3-none-any.whl", hash = "sha256:d22731364c07d72eea0a0ad45bafb2c2937ab6fd38a3507bf55eae8744aa7d85"}, - {file = "toolz-0.12.1.tar.gz", hash = "sha256:ecca342664893f177a13dac0e6b41cbd8ac25a358e5f215316d43e2100224f4d"}, + {file = "toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236"}, + {file = "toolz-1.0.0.tar.gz", hash = "sha256:2c86e3d9a04798ac556793bced838816296a2f085017664e4995cb40a1047a02"}, ] [[package]] name = "tqdm" version = "4.66.5" description = "Fast, Extensible Progress Meter" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2727,7 +2776,6 @@ telegram = ["requests"] name = "traitlets" version = "5.14.3" description = "Traitlets Python configuration system" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2743,7 +2791,6 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, name = "typed-ast" version = "1.4.3" description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" optional = false python-versions = "*" files = [ @@ -2783,7 +2830,6 @@ files = [ name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2793,14 +2839,13 @@ files = [ [[package]] name = "urllib3" -version = "1.26.19" +version = "1.26.20" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, - {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, ] [package.extras] @@ -2810,14 +2855,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.26.3" +version = "20.26.6" description = "Virtual Python Environment builder" -category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, + {file = "virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2"}, + {file = "virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48"}, ] [package.dependencies] @@ -2833,7 +2877,6 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess name = "wcwidth" version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -2845,7 +2888,6 @@ files = [ name = "wrapt" version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2925,7 +2967,6 @@ files = [ name = "wrapt-timeout-decorator" version = "1.5.1" description = "The better timout decorator" -category = "main" optional = false python-versions = ">=3.8.0" files = [ @@ -2951,7 +2992,6 @@ test = ["black", "codecov", "coloredlogs", "coverage", "flake8", "mypy", "pytest name = "xxhash" version = "2.0.2" description = "Python binding for xxHash" -category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3025,7 +3065,6 @@ files = [ name = "zipfile-deflate64" version = "0.2.0" description = "Extract Deflate64 ZIP archives with Python's zipfile API." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3049,21 +3088,24 @@ files = [ [[package]] name = "zipp" -version = "3.20.0" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, - {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.11" -content-hash = "c7cd5352d183e1641123a6c066615245e2bd5160854dae25d00c519189e16a88" +content-hash = "eeac29b154c59b17959559d2e86dae8378598062ed2eba88f4c8d3accd87d773" diff --git a/pyproject.toml b/pyproject.toml index 94db71f..d531b78 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "reaction_utils" -version = "1.6.0" +version = "1.7.0" description = "Utilities for working with reactions, reaction templates and template extraction" authors = ["Genheden, Samuel ", "Kannas, Christos "] license = "Apache-2.0" @@ -28,6 +28,8 @@ numpy = "^1.0.0" rdkit = "^2023.9.1" cgrtools = "^4.1.35" scipy = "^1.11.4" +pydantic = "^2.8.2" +apted = "^1.0.3" [tool.poetry.dev-dependencies] pytest = "^6.2.2" diff --git a/rxnutils/chem/augmentation.py b/rxnutils/chem/augmentation.py new file mode 100644 index 0000000..7e9fd83 --- /dev/null +++ b/rxnutils/chem/augmentation.py @@ -0,0 +1,22 @@ +""" Routines for augmenting chemical reactions +""" + +_SINGLE_REACTANT_REAGENTS = {"10.1.1": "Br", "10.1.2": "Cl"} + + +def single_reactant_augmentation(smiles: str, classification: str) -> str: + """ + Augment single-reactant reaction with additional reagent if possible + based on the classification of the reaction + :param smiles: the reaction SMILES to augment + :param classification: the classification of the reaction or an empty string + :return: the processed SMILES + """ + reactants = smiles.split(">")[0] + if "." in reactants: + return smiles + classification = classification.split(" ")[0] + new_reactant = _SINGLE_REACTANT_REAGENTS.get(classification) + if new_reactant: + return new_reactant + "." + smiles + return smiles diff --git a/rxnutils/chem/cgr.py b/rxnutils/chem/cgr.py index bff91b4..13eac3a 100644 --- a/rxnutils/chem/cgr.py +++ b/rxnutils/chem/cgr.py @@ -4,9 +4,9 @@ import warnings from typing import List -from CGRtools.files.SDFrw import SDFRead -from CGRtools.containers.reaction import ReactionContainer from CGRtools.containers.molecule import MoleculeContainer +from CGRtools.containers.reaction import ReactionContainer +from CGRtools.files.SDFrw import SDFRead from rdkit import Chem from rxnutils.chem.reaction import ChemicalReaction diff --git a/rxnutils/chem/disconnection_sites/atom_map_tagging.py b/rxnutils/chem/disconnection_sites/atom_map_tagging.py index 83f7c2b..e705d26 100644 --- a/rxnutils/chem/disconnection_sites/atom_map_tagging.py +++ b/rxnutils/chem/disconnection_sites/atom_map_tagging.py @@ -95,6 +95,30 @@ def get_atom_list(reactants_smiles: str, product_smiles: str) -> List[int]: return atom_list +def atom_map_tag_reactants(mapped_rxn: str) -> str: + """ + Given atom-mapped reaction, returns disconnection site-tagged reactants where atoms + with changed atom environment are represented by [:1]. + + :param mapped_rxn: Atom-mapped reaction SMILES + :return: SMILES of the reactants containing tags corresponding to atoms changed in the + reaction. + """ + reactants_smiles, _, product_smiles = mapped_rxn.split(">") + + reactants_mol = Chem.MolFromSmiles(reactants_smiles) + atom_list = get_atom_list(reactants_smiles, product_smiles) + + # Set atoms in product with a different environment in reactants to 1 + for atom in reactants_mol.GetAtoms(): + if atom.GetAtomMapNum() in atom_list: + atom.SetAtomMapNum(1) + else: + atom.SetAtomMapNum(0) + + return Chem.MolToSmiles(reactants_mol) + + def atom_map_tag_products(mapped_rxn: str) -> str: """ Given atom-mapped reaction, returns disconnection site-tagged product where atoms diff --git a/rxnutils/chem/disconnection_sites/tag_converting.py b/rxnutils/chem/disconnection_sites/tag_converting.py index 152e789..154a675 100644 --- a/rxnutils/chem/disconnection_sites/tag_converting.py +++ b/rxnutils/chem/disconnection_sites/tag_converting.py @@ -15,10 +15,16 @@ def smiles_tokens(smiles: str) -> List[str]: :param smiles: SMILES to tokenize :return: List of tokens identified in SMILES. """ - pattern = r"(\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\/|:|~|@|\?|>|\*|\!|\$|\%[0-9]{2}|[0-9])" + pattern = r"(\[[^\]]+]|Br?|Cl?|N|O|S|P|F|I|b|c|n|o|s|p|\(|\)|\.|=|#|-|\+|\\\\|\\|\/|:|~|@|\?|>|\*|\!|\$|\%[0-9]{2}|[0-9])" regex = re.compile(pattern) tokens = [token for token in regex.findall(smiles)] - assert smiles == "".join(tokens) + + tokenized_smiles = "".join(tokens) + if smiles != tokenized_smiles: + raise AssertionError( + f"tokenized SMILES not the same as input SMILES: {tokenized_smiles}, " + "{smiles}, tokens: {tokens}" + ) return tokens @@ -68,8 +74,6 @@ def tagged_smiles_from_tokens( reaction using "!", and SMILES of the (reconstructed) untagged product """ - print(product_tagged_tokens) - product_converted = "" product_untagged = "" diff --git a/rxnutils/chem/reaction.py b/rxnutils/chem/reaction.py index 6d9e22b..3aea3a5 100644 --- a/rxnutils/chem/reaction.py +++ b/rxnutils/chem/reaction.py @@ -1,19 +1,16 @@ """Module containing a class to handle chemical reactions""" import hashlib -from typing import List, Tuple, Optional, Dict, Any +from typing import Any, Dict, List, Optional, Tuple import wrapt_timeout_decorator +from rdchiral import template_extractor as extractor from rdkit import Chem from rdkit.Chem import AllChem -from rdchiral import template_extractor as extractor -from rxnutils.chem.rinchi import rinchi_api from rxnutils.chem import utils +from rxnutils.chem.rinchi import rinchi_api from rxnutils.chem.template import ReactionTemplate -from rxnutils.chem.utils import ( - reassign_rsmi_atom_mapping, - split_smiles_from_reaction, -) +from rxnutils.chem.utils import reassign_rsmi_atom_mapping, split_smiles_from_reaction class ReactionException(Exception): diff --git a/rxnutils/chem/rinchi/download_rinchi.py b/rxnutils/chem/rinchi/download_rinchi.py index bd2c993..6ef9f1a 100644 --- a/rxnutils/chem/rinchi/download_rinchi.py +++ b/rxnutils/chem/rinchi/download_rinchi.py @@ -1,9 +1,9 @@ """Module for downloading InChI Trust Reaction InChI.""" +import logging import os -import sys import stat +import sys from zipfile import ZipFile -import logging import requests diff --git a/rxnutils/chem/rinchi/rinchi_api.py b/rxnutils/chem/rinchi/rinchi_api.py index 57d6ac3..d34e17f 100644 --- a/rxnutils/chem/rinchi/rinchi_api.py +++ b/rxnutils/chem/rinchi/rinchi_api.py @@ -1,16 +1,15 @@ """Module containing an API to the Reaction InChI program""" import logging import os -import sys import subprocess +import sys import tempfile from collections import namedtuple from rdkit.Chem import AllChem from rxnutils.chem.rinchi import download_rinchi -from rxnutils.chem.rinchi.download_rinchi import RInChIError, PLATFORM2FOLDER - +from rxnutils.chem.rinchi.download_rinchi import PLATFORM2FOLDER, RInChIError RInChIStructure = namedtuple( "RInChI", "rinchi rauxinfo long_rinchikey short_rinchikey web_rinchikey" diff --git a/rxnutils/chem/template.py b/rxnutils/chem/template.py index daa9522..5b306c8 100644 --- a/rxnutils/chem/template.py +++ b/rxnutils/chem/template.py @@ -1,18 +1,17 @@ """Module containing useful representations of templates """ -import re import hashlib import logging +import re from collections import defaultdict from itertools import permutations -from typing import List, Dict, Set, Iterator, Tuple, Any +from typing import Any, Dict, Iterator, List, Set, Tuple import numpy as np import rdchiral.main as rdc -from xxhash import xxh32 from rdkit import Chem, DataStructs from rdkit.Chem import AllChem, SanitizeFlags # pylint: disable=all - +from xxhash import xxh32 DELIM_REGEX_STR = r"[&:\]]" AROMATIC_REGEX_STR = r"&a" + DELIM_REGEX_STR diff --git a/rxnutils/chem/utils.py b/rxnutils/chem/utils.py index bc3982d..93dcd48 100644 --- a/rxnutils/chem/utils.py +++ b/rxnutils/chem/utils.py @@ -1,12 +1,9 @@ """Module containing various chemical utility routines""" - -import logging import functools +import logging from typing import List, Tuple - import rdchiral.template_extractor - from rdkit import Chem from rdkit.Chem import AllChem from rdkit.Chem.MolStandardize import rdMolStandardize @@ -292,7 +289,7 @@ def get_special_groups(mol) -> List[Tuple[Tuple[int, ...], Tuple[int, ...]]]: # Build list groups = [] - for add_if_match, template in group_templates: + for (add_if_match, template) in group_templates: matches = mol.GetSubstructMatches( Chem.MolFromSmarts(template), useChirality=True ) diff --git a/rxnutils/data/base_pipeline.py b/rxnutils/data/base_pipeline.py index 91df7ae..63364aa 100644 --- a/rxnutils/data/base_pipeline.py +++ b/rxnutils/data/base_pipeline.py @@ -1,15 +1,14 @@ """Module containing base class for data pipelines """ -import os import math +import os from pathlib import Path +from typing import List, Tuple import pandas as pd from metaflow import FlowSpec, Parameter -from typing import List, Tuple - -from rxnutils.data.batch_utils import create_csv_batches, combine_csv_batches +from rxnutils.data.batch_utils import combine_csv_batches, create_csv_batches # This is hack to only import the validation_runner if rxnmapper is not installed try: diff --git a/rxnutils/data/mapping_pipeline.py b/rxnutils/data/mapping_pipeline.py index 56c1383..53af1dc 100644 --- a/rxnutils/data/mapping_pipeline.py +++ b/rxnutils/data/mapping_pipeline.py @@ -4,7 +4,7 @@ """ from pathlib import Path -from metaflow import step, Parameter +from metaflow import Parameter, step from rxnutils.data.base_pipeline import DataBaseFlow from rxnutils.data.mapping import main as map_data diff --git a/rxnutils/data/ord/import_ord_dataset.py b/rxnutils/data/ord/import_ord_dataset.py index 5ec93cb..9808e53 100644 --- a/rxnutils/data/ord/import_ord_dataset.py +++ b/rxnutils/data/ord/import_ord_dataset.py @@ -1,9 +1,9 @@ """ Module containing script to import ORD dataset to a CSV file """ -import re import argparse import os +import re from collections import defaultdict from typing import Optional, Sequence diff --git a/rxnutils/data/ord/preparation_pipeline.py b/rxnutils/data/ord/preparation_pipeline.py index 358d450..f76e119 100644 --- a/rxnutils/data/ord/preparation_pipeline.py +++ b/rxnutils/data/ord/preparation_pipeline.py @@ -6,7 +6,7 @@ import os from pathlib import Path -from metaflow import step, Parameter +from metaflow import Parameter, step from rxnutils.data.base_pipeline import DataPreparationBaseFlow from rxnutils.data.ord.import_ord_dataset import main as import_data diff --git a/rxnutils/data/uspto/combine.py b/rxnutils/data/uspto/combine.py index c162d89..350f975 100644 --- a/rxnutils/data/uspto/combine.py +++ b/rxnutils/data/uspto/combine.py @@ -5,12 +5,15 @@ * preserve the ReactionSmiles and Year columns * create an ID from PatentNumber and ParagraphNum and row index in the original file """ + import argparse from pathlib import Path from typing import Optional, Sequence import pandas as pd +from rxnutils.data.uspto.uspto_yield import UsptoYieldCuration + DEFAULT_FILENAMES = [ "1976_Sep2016_USPTOgrants_smiles.rsmi", "2001_Sep2016_USPTOapplications_smiles.rsmi", @@ -29,6 +32,12 @@ def main(args: Optional[Sequence[str]] = None) -> None: "--output", default="uspto_data.csv", help="the output filename" ) parser.add_argument("--folder", default=".", help="folder with downloaded files") + parser.add_argument( + "--with_yields", + action="store_true", + default=False, + help="if to add yield columns", + ) args = parser.parse_args(args) filenames = [Path(args.folder) / filename for filename in args.filenames] @@ -42,11 +51,18 @@ def main(args: Optional[Sequence[str]] = None) -> None: para_num = data["ParagraphNum"].fillna("") row_num = data.index.astype(str) data["ID"] = data["PatentNumber"] + ";" + para_num + ";" + row_num - data2 = data[["ID", "Year", "ReactionSmiles"]] + columns = ["ID", "Year", "ReactionSmiles"] + if args.with_yields: + columns += ["TextMinedYield", "CalculatedYield"] + data2 = data[columns] print(f"Total number of unique IDs: {len(set(data2['ID']))}") print(f"Total number of records: {len(data2)}") + if args.with_yields: + print("Curating yields...") + data2 = UsptoYieldCuration()(data2) + data2.to_csv(Path(args.folder) / args.output, sep="\t", index=False) diff --git a/rxnutils/data/uspto/download.py b/rxnutils/data/uspto/download.py index 8719772..edf4165 100644 --- a/rxnutils/data/uspto/download.py +++ b/rxnutils/data/uspto/download.py @@ -1,14 +1,13 @@ """Module containing a script to download USPTO files Figshare """ -import os import argparse +import os from pathlib import Path from typing import Optional, Sequence -import tqdm -import requests import py7zr - +import requests +import tqdm FILES_TO_DOWNLOAD = [ { diff --git a/rxnutils/data/uspto/preparation_pipeline.py b/rxnutils/data/uspto/preparation_pipeline.py index a28df43..348acf0 100644 --- a/rxnutils/data/uspto/preparation_pipeline.py +++ b/rxnutils/data/uspto/preparation_pipeline.py @@ -2,13 +2,14 @@ Module containing pipeline for downloading, transforming and cleaning USPTO data This needs to be run in an environment with rxnutils installed """ + from pathlib import Path from metaflow import step from rxnutils.data.base_pipeline import DataPreparationBaseFlow -from rxnutils.data.uspto.download import main as download_uspto from rxnutils.data.uspto.combine import main as combine_uspto +from rxnutils.data.uspto.download import main as download_uspto class UsptoDataPreparationFlow(DataPreparationBaseFlow): diff --git a/rxnutils/data/uspto/uspto_yield.py b/rxnutils/data/uspto/uspto_yield.py new file mode 100644 index 0000000..c5c2a79 --- /dev/null +++ b/rxnutils/data/uspto/uspto_yield.py @@ -0,0 +1,51 @@ +""" +Code for curating USPTO yields. + +Inspiration from this code: https://github.com/DocMinus/Yield_curation_USPTO + +This could potentially be an action, but since it only make sens to use it +with USPTO data, it resides here for now. +""" + +from dataclasses import dataclass + +import numpy as np +import pandas as pd + + +@dataclass +class UsptoYieldCuration: + """ + Action for curating USPTO yield columns + """ + + text_yield_column: str = "TextMinedYield" + calc_yield_column: str = "CalculatedYield" + out_column: str = "CuratedYield" + + def __call__(self, data: pd.DataFrame) -> pd.DataFrame: + calc_yield = data[self.calc_yield_column].str.rstrip("%") + calc_yield = pd.to_numeric(calc_yield, errors="coerce") + calc_yield[(calc_yield < 0) | (calc_yield > 100)] = np.nan + + text_yield = data[self.text_yield_column].str.lstrip("~") + text_yield = text_yield.str.rstrip("%") + text_yield = text_yield.str.replace(">=", "", regex=False) + text_yield = text_yield.str.replace(">", "", regex=False) + text_yield = text_yield.str.replace("<", "", regex=False) + text_yield = text_yield.str.replace(r"\d{1,2}\sto\s", "", regex=True) + text_yield = pd.to_numeric(text_yield, errors="coerce") + text_yield[(text_yield < 0) | (text_yield > 100)] = np.nan + + curated_yield = text_yield.copy() + + sel = (~calc_yield.isna()) & (~text_yield.isna()) + curated_yield[sel] = np.maximum(calc_yield[sel], text_yield[sel]) + + sel = (~calc_yield.isna()) & (text_yield.isna()) + curated_yield[sel] = calc_yield[sel] + + return data.assign(**{self.out_column: curated_yield}) + + def __str__(self) -> str: + return f"{self.pretty_name} (create one column with curated yield values)" diff --git a/rxnutils/pipeline/actions/reaction_mod.py b/rxnutils/pipeline/actions/reaction_mod.py index 5fe65c6..8d16211 100644 --- a/rxnutils/pipeline/actions/reaction_mod.py +++ b/rxnutils/pipeline/actions/reaction_mod.py @@ -18,9 +18,9 @@ from rxnutils.chem.disconnection_sites.tag_converting import convert_atom_map_tag from rxnutils.chem.utils import ( atom_mapping_numbers, + desalt_molecules, neutralize_molecules, remove_atom_mapping, - desalt_molecules, ) from rxnutils.pipeline.base import ReactionActionMixIn, action, global_apply diff --git a/rxnutils/pipeline/actions/reaction_props.py b/rxnutils/pipeline/actions/reaction_props.py index aeb6ce8..67f3400 100644 --- a/rxnutils/pipeline/actions/reaction_props.py +++ b/rxnutils/pipeline/actions/reaction_props.py @@ -5,7 +5,7 @@ import os from collections import defaultdict from dataclasses import dataclass -from typing import ClassVar, List, Set, Tuple, Optional +from typing import ClassVar, List, Optional, Set, Tuple import pandas as pd from rdkit import Chem, RDLogger @@ -14,11 +14,8 @@ from rdkit.Chem.rdMolDescriptors import CalcNumRings from rdkit.Chem.rdmolops import FindPotentialStereo -from rxnutils.pipeline.base import ( - action, - global_apply, - ReactionActionMixIn, -) +from rxnutils.chem.cgr import CondensedGraphReaction +from rxnutils.chem.reaction import ChemicalReaction from rxnutils.chem.utils import ( atom_mapping_numbers, has_atom_mapping, @@ -26,8 +23,7 @@ reaction_centres, split_smiles_from_reaction, ) -from rxnutils.chem.reaction import ChemicalReaction -from rxnutils.chem.cgr import CondensedGraphReaction +from rxnutils.pipeline.base import ReactionActionMixIn, action, global_apply rd_logger = RDLogger.logger() rd_logger.setLevel(RDLogger.CRITICAL) diff --git a/rxnutils/pipeline/actions/templates.py b/rxnutils/pipeline/actions/templates.py index 3261cb4..c0f4cc5 100644 --- a/rxnutils/pipeline/actions/templates.py +++ b/rxnutils/pipeline/actions/templates.py @@ -1,16 +1,16 @@ """Module containing template validation actions""" from __future__ import annotations + from dataclasses import dataclass -from typing import ClassVar, Set, Sequence +from typing import ClassVar, Sequence, Set import pandas as pd -from rdkit import RDLogger -from rdkit import Chem +from rdkit import Chem, RDLogger from rdkit.Chem import AllChem -from rxnutils.pipeline.base import action, global_apply from rxnutils.chem.template import ReactionTemplate from rxnutils.chem.utils import split_smiles_from_reaction +from rxnutils.pipeline.base import action, global_apply rd_logger = RDLogger.logger() rd_logger.setLevel(RDLogger.CRITICAL) diff --git a/rxnutils/pipeline/base.py b/rxnutils/pipeline/base.py index 35a9684..57d5316 100644 --- a/rxnutils/pipeline/base.py +++ b/rxnutils/pipeline/base.py @@ -1,16 +1,15 @@ """Module containing routines for the validation framework""" from __future__ import annotations -from typing import Any -from typing import Callable -from typing import Dict, Optional, Tuple, List + from collections import defaultdict from dataclasses import fields +from typing import Any, Callable, Dict, List, Optional, Tuple import pandas as pd import swifter # noqa #pylint: disable=unused-import from tqdm import tqdm -from rxnutils.chem.utils import split_smiles_from_reaction, join_smiles_from_reaction +from rxnutils.chem.utils import join_smiles_from_reaction, split_smiles_from_reaction ActionType = Callable[[pd.DataFrame], pd.DataFrame] diff --git a/rxnutils/pipeline/runner.py b/rxnutils/pipeline/runner.py index 846e996..2e48949 100644 --- a/rxnutils/pipeline/runner.py +++ b/rxnutils/pipeline/runner.py @@ -1,18 +1,19 @@ """Module containg routines and interface to run pipelines""" import argparse -from typing import Dict, Any, Optional, Sequence +from typing import Any, Dict, Optional, Sequence -import yaml import pandas as pd +import yaml + +import rxnutils.pipeline.actions.dataframe_mod # noqa # imports needed to register all the actions # pylint: disable=unused-import import rxnutils.pipeline.actions.reaction_mod # noqa import rxnutils.pipeline.actions.reaction_props # noqa import rxnutils.pipeline.actions.templates # noqa -import rxnutils.pipeline.actions.dataframe_mod # noqa -from rxnutils.pipeline.base import create_action, global_apply, list_actions from rxnutils.data.batch_utils import read_csv_batch +from rxnutils.pipeline.base import create_action, global_apply, list_actions def run_pipeline( diff --git a/rxnutils/routes/base.py b/rxnutils/routes/base.py index 6f0425f..35ffd87 100644 --- a/rxnutils/routes/base.py +++ b/rxnutils/routes/base.py @@ -4,43 +4,50 @@ and drawing the route """ -from typing import Dict, Any, List, Callable, Union +import warnings from copy import deepcopy from operator import itemgetter +from typing import Any, Callable, Dict, List, Set, Tuple, Union import pandas as pd from PIL.Image import Image as PilImage from rdkit import Chem -from rxnutils.pipeline.actions.reaction_mod import NameRxn, RxnMapper -from rxnutils.routes.image import RouteImageFactory +from rxnutils.chem.augmentation import single_reactant_augmentation from rxnutils.chem.utils import ( atom_mapping_numbers, - split_smiles_from_reaction, join_smiles_from_reaction, + split_smiles_from_reaction, ) +from rxnutils.pipeline.actions.reaction_mod import NameRxn, RxnMapper +from rxnutils.routes.image import RouteImageFactory +from rxnutils.routes.utils.validation import validate_dict class SynthesisRoute: """ This encapsulates a synthesis route or a reaction tree. - It provide convenient methods for assigning atom-mapping + It provide convinient methods for assigning atom-mapping to the reactions, and for providing reaction-level data of the route It is typically initiallized by one of the readers in the `rxnutils.routes.readers` module. - The tree depth and the forward step are automatically assigned + The tree depth and the forward step is automatically assigned to each reaction node. + The `max_depth` attribute holds the longest-linear-sequence (LLS) + :param reaction_tree: the tree structure representing the route """ def __init__(self, reaction_tree: Dict[str, Any]) -> None: + validate_dict(reaction_tree) self.reaction_tree = reaction_tree self.max_depth = _assign_tree_depth(reaction_tree) _assign_forward_step(reaction_tree, self.max_depth) + self._nsteps = -1 @property def mapped_root_smiles(self) -> str: @@ -61,6 +68,13 @@ def mapped_root_smiles(self) -> str: return first_reaction["metadata"]["mapped_reaction_smiles"].split(">")[-1] + @property + def nsteps(self) -> int: + """Return the number of reactions in the route""" + if self._nsteps == -1: + self._nsteps = len(self.reaction_smiles()) + return self._nsteps + def atom_mapped_reaction_smiles(self) -> List[str]: """Returns a list of the atom-mapped reaction SMILES in the route""" smiles = [] @@ -68,9 +82,7 @@ def atom_mapped_reaction_smiles(self) -> List[str]: return smiles def assign_atom_mapping( - self, - overwrite: bool = False, - only_rxnmapper: bool = False, + self, overwrite: bool = False, only_rxnmapper: bool = False ) -> None: """ Assign atom-mapping to each reaction in the route and @@ -138,18 +150,49 @@ def chains( mol["chain"] = "sub1" return chains - def image(self, show_atom_mapping=False) -> PilImage: + def image( + self, show_atom_mapping: bool = False, factory_kwargs: Dict[str, Any] = None + ) -> PilImage: """ Depict the route. :param show_atom_mapping: if True, will show the atom-mapping + :param factory_kwargs: additional keyword arguments sent to the `RouteImageFactory` :returns: the image of the route """ + factory_kwargs = factory_kwargs or {} if show_atom_mapping: dict_ = self._create_atom_mapping_tree() else: dict_ = self.reaction_tree - return RouteImageFactory(dict_).image + return RouteImageFactory(dict_, **factory_kwargs).image + + def is_solved(self) -> bool: + """ + Find if this route is solved, i.e. if all starting material + is in stock. + + To be accurate, each molecule node need to have an extra + boolean property called `in_stock`. + """ + try: + _find_leaves_not_in_stock(self.reaction_tree) + except ValueError: + return False + return True + + def leaves( + self, + ) -> Set[str]: + """ + Extract a set with the SMILES of all the leaf nodes, i.e. + starting material + + :return: a set of SMILES strings + """ + leaves = set() + _collect_leaves(self.reaction_tree, leaves) + return leaves def reaction_data(self) -> List[Dict[str, Any]]: """ @@ -161,13 +204,41 @@ def reaction_data(self) -> List[Dict[str, Any]]: _collect_reaction_data(self.reaction_tree, data) return data - def reaction_smiles(self) -> List[str]: - """Returns a list of the un-mapped reaction SMILES""" - smiles = [] - _collect_reaction_smiles(self.reaction_tree, smiles) - return smiles + def reaction_ngrams(self, nitems: int, metadata_key: str) -> List[Tuple[Any, ...]]: + """ + Extract an n-gram representation of the route by building up n-grams + of the reaction metadata. - def remap(self, other: "SynthesisRoute") -> None: + :param nitems: the length of the gram + :param metadata_key: the metadata to extract + :return: the collected n-grams + """ + if self.max_depth < nitems: + return [] + first_reaction = self.reaction_tree["children"][0] + ngrams = [] + _collect_ngrams(first_reaction, nitems, metadata_key, ngrams) + return ngrams + + def reaction_smiles(self, augment: bool = False) -> List[str]: + """ + Returns a list of the un-mapped reaction SMILES + :param augment: if True will add reagents to single-reactant + reagents whenever possible + """ + if not augment: + smiles_list = [] + _collect_reaction_smiles(self.reaction_tree, smiles_list) + return smiles_list + + smiles_list = [] + for data in self.reaction_data(): + smiles = data["reaction_smiles"] + classification = data.get("classification", "") + smiles_list.append(single_reactant_augmentation(smiles, classification)) + return smiles_list + + def remap(self, other: Union["SynthesisRoute", str, Dict[int, int]]) -> None: """ Remap the reaction so that it follows the mapping of a 1) root compound in a reference route, 2) a ref compound given @@ -206,7 +277,18 @@ def _assign_mapping( df = pd.DataFrame({"smiles": list(set(self.reaction_smiles()))}) nextmove_action = NameRxn(in_column="smiles", nm_rxn_column="mapped_smiles") rxnmapper_action = RxnMapper(in_column="smiles") - df = rxnmapper_action(nextmove_action(df)) + if not only_rxnmapper: + try: + df = nextmove_action(df) + # Raised by nextmove_action if namerxn not in path + except FileNotFoundError: + df = df.assign(NMC=["0.0"] * len(df), mapped_smiles=[""] * len(df)) + warnings.warn( + "namerxn does not appear to be in $PATH. Run failed and proceeding with rxnmapper only" + ) + else: + df = df.assign(NMC=["0.0"] * len(df), mapped_smiles=[""] * len(df)) + df = rxnmapper_action(df) if only_rxnmapper: df["mapped_smiles"] = df["RxnmapperRxnSmiles"] else: @@ -349,7 +431,7 @@ def smiles2inchikey(smiles: str, ignore_stereo: bool = False) -> str: def _assign_atom_mapped_smiles( - tree_dict: Dict[str, Any], reaction_smiles: str = "" + tree_dict: Dict[str, Any], reactants_smiles: List[str] = None ) -> None: """ Used to copy the atom-mapped SMILES from the reaction metadata @@ -360,23 +442,26 @@ def _assign_atom_mapped_smiles( based on partial InChI key without stereoinformation :param tree_dict: the current molecule node - :param reaction_smiles: the reaction SMILES of the parent reaction + :param reactants_smiles: the list of reactants SMILES of the parent reaction """ tree_dict["unmapped_smiles"] = tree_dict["smiles"] # For leaf nodes if not tree_dict.get("children", []): - inchi2mapped = { - smiles2inchikey(smiles, ignore_stereo=True): smiles - for smiles in split_smiles_from_reaction(reaction_smiles.split(">")[0]) - } inchi = smiles2inchikey(tree_dict["smiles"], ignore_stereo=True) - tree_dict["smiles"] = inchi2mapped.get(inchi, tree_dict["smiles"]) + found = -1 + for idx, smi in enumerate(reactants_smiles): + if inchi == smiles2inchikey(smi, ignore_stereo=True): + found = idx + break + if found > -1: + tree_dict["smiles"] = reactants_smiles.pop(found) else: reaction_dict = tree_dict["children"][0] reaction_smiles = reaction_dict["metadata"]["mapped_reaction_smiles"] tree_dict["smiles"] = reaction_smiles.split(">")[-1] + reactants_smiles = split_smiles_from_reaction(reaction_smiles.split(">")[0]) for grandchild in reaction_dict["children"]: - _assign_atom_mapped_smiles(grandchild, reaction_smiles) + _assign_atom_mapped_smiles(grandchild, reactants_smiles) def _assign_forward_step( @@ -435,6 +520,58 @@ def _collect_atom_mapped_smiles(tree_dict: Dict[str, Any], smiles: List[str]) -> _collect_atom_mapped_smiles(grandchild, smiles) +def _collect_leaves(tree_dict: Dict[str, Any], leaves: Set[str]) -> None: + """ + Traverse the tree and collect SMILES of molecule nodes that has no children + + :param tree_dict: the current molecule node + :param leaves: the list of collected leaves + """ + children = tree_dict.get("children", []) + if children: + for child in children: + _collect_leaves(child, leaves) + else: + leaves.add(tree_dict["smiles"]) + + +def _collect_ngrams( + tree_dict: Dict[str, Any], + nitems: int, + metadata_key: str, + result: List[Tuple[Any, ...]], + accumulation: List[str] = None, +): + """ + Collect ngrams from reaction metadata + + :param tree_dict: the current reaction node in the recursion + :param nitems: the length of the gram + :param metadata_key: the metadata to extract + :param result: the collected ngrams + :param accumulation: the accumulate items in the recursion + :raise ValueError: if this routine is initialized from a molecule node + """ + accumulation = accumulation or [] + if tree_dict["type"] == "mol": + raise ValueError( + "Found _collect_ngrams at molecule node. This should not happen." + ) + + data = tree_dict.get("metadata", {}).get(metadata_key) + accumulation.append(data) + + if len(accumulation) == nitems: + result.append(tuple(accumulation)) + accumulation.pop(0) + + for mol_child in tree_dict["children"]: + for rxn_grandchild in mol_child.get("children", []): + _collect_ngrams( + rxn_grandchild, nitems, metadata_key, result, list(accumulation) + ) + + def _collect_reaction_data( tree_dict: Dict[str, Any], data: List[Dict[str, Any]] ) -> None: @@ -553,6 +690,20 @@ def _extract_chain_molecules( chain_molecules.append(mol_dict) +def _find_leaves_not_in_stock(tree_dict: Dict[str, Any]) -> None: + """ + Traverse the tree and check the `in_stock` value of molecule + nodes without children (leaves). If one is found that is not + in stock this raises an exception, which stops the traversal. + """ + children = tree_dict.get("children", []) + if not children and not tree_dict.get("in_stock", True): + raise ValueError(f"child not in stock {tree_dict}") + elif children: + for child in children: + _find_leaves_not_in_stock(child) + + def _inherit_atom_mapping( tree_dict: Dict[str, Any], new_atomnum: int, parent_smiles: str = "" ) -> None: diff --git a/rxnutils/routes/comparison.py b/rxnutils/routes/comparison.py new file mode 100644 index 0000000..4e0bd70 --- /dev/null +++ b/rxnutils/routes/comparison.py @@ -0,0 +1,222 @@ +""" Contains routines for computing route similarities +""" +import functools +from typing import Any, Callable, Dict, List, Sequence, Set, Tuple + +import numpy as np + +from rxnutils.chem.reaction import ChemicalReaction +from rxnutils.chem.utils import atom_mapping_numbers +from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.ted.distances_calculator import ted_distances_calculator + +RouteDistancesCalculator = Callable[[Sequence[SynthesisRoute]], np.ndarray] + + +def simple_route_similarity(routes: Sequence[SynthesisRoute]) -> np.ndarray: + """ + Returns the geometric mean of the simple bond forming similarity, and + the atom matching bonanza similarity + + :param routes: the sequence of routes to compare + :return: the pairwise similarity + """ + bond_score = simple_bond_forming_similarity(routes) + atom_score = atom_matching_bonanza_similarity(routes) + return np.sqrt(bond_score * atom_score) + + +def atom_matching_bonanza_similarity(routes: Sequence[SynthesisRoute]) -> np.ndarray: + """ + Calculates the pairwise similarity of a sequence of routes + based on the overlap of the atom-mapping numbers of the compounds + in the routes. + + :param routes: the sequence of routes to compare + :return: the pairwise similarity + """ + sims = np.zeros((len(routes), len(routes))) + for idx1, route1 in enumerate(routes): + mols1 = _extract_atom_mapping_numbers(route1) + n1 = len(mols1) - 1 + mols1 = [mol for mol in mols1 if mol] + for idx2, route2 in enumerate(routes): + if idx1 == idx2: + sims[idx1, idx2] = 1.0 + continue + # If both of the routes have not reactions, we assume + # they are identical and it is assignes a score of 1.0 + if route1.max_depth == 0 and route2.max_depth == 0: + sims[idx1, idx2] = 1.0 + continue + # If just one the route has no reactions, we assume + # they are maximal dissimilar and assigns a score 0.0 (see how `sims` is initialized) + if route1.max_depth == 0 or route2.max_depth == 0: + continue + + mols2 = _extract_atom_mapping_numbers(route2) + n2 = len(mols2) - 1 + mols2 = [mol for mol in mols2 if mol] + + o = _calc_overlap_matrix(mols1[1:], mols2[1:]) + sims[idx1, idx2] = (o.max(axis=1).sum() + o.max(axis=0).sum()) / (n1 + n2) + return sims + + +def simple_bond_forming_similarity(routes: Sequence[SynthesisRoute]) -> np.ndarray: + """ + Calculates the pairwise similarity of a sequence of routes + based on the overlap of formed bonds in the reactions. + + :param routes: the sequence of routes to compare + :return: the pairwise similarity + """ + sims = np.ones((len(routes), len(routes))) + for idx1, route1 in enumerate(routes): + bonds1 = _extract_formed_bonds(route1) + for idx2, route2 in enumerate(routes): + if idx2 <= idx1: + continue + # If both of the routes have not reactions, we assume + # they are identical and it is assignes a score of 1.0 (see how `sims` is initialized) + if route1.max_depth == 0 and route2.max_depth == 0: + continue + # If just one the route has no reactions, we assume + # they are maximal dissimilar and assigns a score 0.0 + if route1.max_depth == 0 or route2.max_depth == 0: + sims[idx1, idx2] = sims[idx2, idx1] = 0.0 + continue + + bonds2 = _extract_formed_bonds(route2) + sims[idx1, idx2] = _bond_formed_overlap_score(bonds1, bonds2) + sims[idx2, idx1] = sims[idx1, idx2] # Score is symmetric + return sims + + +def route_distances_calculator(model: str, **kwargs: Any) -> RouteDistancesCalculator: + """ + Return a callable that given a list routes as dictionaries + calculate the squared distance matrix + + :param model: the route distance model name + :param kwargs: additional keyword arguments for the model + :return: the appropriate route distances calculator + """ + if model not in ["ted", "lstm"]: + raise ValueError("Model must be either 'ted' or 'lstm'") + + if model == "ted": + model_kwargs = _copy_kwargs(["content", "timeout"], **kwargs) + return functools.partial(ted_distances_calculator, **model_kwargs) + + # Placeholder for LSTM distances calculation + # model_kwargs = _copy_kwargs(["model_path"], **kwargs) + # return lstm_distances_calculator(**model_kwargs) + raise NotImplementedError("LSTM route distances calculator not implemented yet.") + + +def _copy_kwargs(keys_to_copy: List[str], **kwargs: Any) -> Dict[str, Any]: + """Copy selected keyword arguments.""" + new_kwargs = {} + for key in keys_to_copy: + if key in kwargs: + new_kwargs[key] = kwargs[key] + return new_kwargs + + +def _calc_overlap_matrix(mols1: List[List[int]], mols2: List[List[int]]) -> np.ndarray: + """ + Calculate the pairwise overlap matrix between + the molecules in the two input vectors + + :param mols1: the atom-mapping numbers of the first molecule + :param mols2: the atom-mapping numbers of the second molecule + :return: the computed matrix + """ + + def mol_overlap(mol1, mol2): + if not mol1 and not mol2: + return 0 + return len(set(mol1).intersection(mol2)) / max(len(mol1), len(mol2)) + + overlaps = np.zeros((len(mols1), len(mols2))) + for idx1, mol1 in enumerate(mols1): + for idx2, mol2 in enumerate(mols2): + overlaps[idx1, idx2] = mol_overlap(mol1, mol2) + return overlaps + + +def _extract_atom_mapping_numbers(route: SynthesisRoute) -> List[List[int]]: + """ + Extract all the compounds in a synthesis routes as a set of + atom-mapping numbers of the atoms in the compounds. + + Only account for atom-mapping numbers that exists in the root compound + """ + if route.max_depth == 0: + return [] + + root_atom_numbers = sorted(atom_mapping_numbers(route.mapped_root_smiles)) + mapping_list = [root_atom_numbers] + for reaction_smiles in route.atom_mapped_reaction_smiles(): + reactants_smiles = reaction_smiles.split(">")[0] + for smi in reactants_smiles.split("."): + atom_numbers = sorted( + [num for num in atom_mapping_numbers(smi) if num in root_atom_numbers] + ) + mapping_list.append(atom_numbers) + return mapping_list + + +def _extract_formed_bonds(route: SynthesisRoute) -> List[Tuple[int, int]]: + """ + Extract a set of bonds formed in the synthesis routes. + Only bonds contributing to the root compound is accounted for. + """ + if route.max_depth == 0: + return [] + + formed_bonds = [] + root_atom_numbers = set(atom_mapping_numbers(route.mapped_root_smiles)) + for reaction_smiles in route.atom_mapped_reaction_smiles(): + rxn_obj = ChemicalReaction(reaction_smiles, clean_smiles=False) + product_bonds = set() + product_bonds = _extract_molecule_bond_tuple(rxn_obj.products) + reactants_bonds = _extract_molecule_bond_tuple(rxn_obj.reactants) + for idx1, idx2 in product_bonds - reactants_bonds: + if idx1 in root_atom_numbers and idx2 in root_atom_numbers: + formed_bonds.append(tuple(sorted([idx1, idx2]))) + return formed_bonds + + +def _extract_molecule_bond_tuple(molecules: List) -> Set[int]: + """ + Returns the sorted set of atom-mapping number pairs for each + bonds in a list of rdkit molecules + """ + bonds = set() + for bond in [bond for mol in molecules for bond in mol.GetBonds()]: + bonds.add( + tuple( + sorted( + [ + bond.GetBeginAtom().GetAtomMapNum(), + bond.GetEndAtom().GetAtomMapNum(), + ] + ) + ) + ) + return bonds + + +def _bond_formed_overlap_score( + bonds1: List[Tuple[int]], bonds2: List[Tuple[int]] +) -> float: + """ + Computes a similarity score of two routes by comparing the overlap + of bonds formed in the synthesis routes. + """ + overlap = len(set(bonds1).intersection(bonds2)) + if not bonds1 and not bonds2: + return 0 + return overlap / max(len(bonds1), len(bonds2)) diff --git a/rxnutils/routes/image.py b/rxnutils/routes/image.py index 03f3035..53eb048 100644 --- a/rxnutils/routes/image.py +++ b/rxnutils/routes/image.py @@ -11,15 +11,7 @@ if TYPE_CHECKING: # pylint: disable=ungrouped-imports - from typing import ( - Any, - Optional, - Dict, - List, - Sequence, - Tuple, - Union, - ) + from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union PilColor = Union[str, Tuple[int, int, int]] FrameColors = Optional[Dict[bool, PilColor]] @@ -47,6 +39,7 @@ def molecules_to_images( mols: Sequence[Chem.rdchem.Mol], frame_colors: Sequence[PilColor], size: int = 300, + draw_kwargs: Dict[str, Any] = None, ) -> List[PilImage]: """ Create pretty images of molecules with a colored frame around each one of them. @@ -56,12 +49,12 @@ def molecules_to_images( :param smiles_list: the molecules :param frame_colors: the color of the frame for each molecule :param size: the sub-image size + :param draw_kwargs: additional keyword-arguments sent to `MolsToGridImage` :return: the produced images """ + draw_kwargs = draw_kwargs or {} all_mols = Draw.MolsToGridImage( - mols, - molsPerRow=len(mols), - subImgSize=(size, size), + mols, molsPerRow=len(mols), subImgSize=(size, size), **draw_kwargs ) if not hasattr(all_mols, "crop"): # Is not a PIL image fileobj = io.BytesIO(all_mols.data) @@ -153,6 +146,9 @@ class RouteImageFactory: :param in_stock_colors: the colors around molecules, defaults to {True: "green", False: "orange"} :param show_all: if True, also show nodes that are marked as hidden :param margin: the margin between images + :param mol_size: the size of the molecule + :param mol_draw_kwargs: additional arguments sent to the drawing routine + :param replace_mol_func: an optional function to replace molecule images """ def __init__( @@ -161,6 +157,9 @@ def __init__( in_stock_colors: FrameColors = None, show_all: bool = True, margin: int = 100, + mol_size: int = 300, + mol_draw_kwargs: Dict[str, Any] = None, + replace_mol_func: Callable[[Dict[str, Any]], None] = None, ) -> None: in_stock_colors = in_stock_colors or { True: "green", @@ -172,9 +171,13 @@ def __init__( self._stock_lookup: Dict[str, Any] = {} self._mol_lookup: Dict[str, Any] = {} self._extract_molecules(route) + if replace_mol_func is not None: + replace_mol_func(self._mol_lookup) images = molecules_to_images( list(self._mol_lookup.values()), [in_stock_colors[val] for val in self._stock_lookup.values()], + size=mol_size, + draw_kwargs=mol_draw_kwargs or {}, ) self._image_lookup = dict(zip(self._mol_lookup.keys(), images)) diff --git a/rxnutils/routes/readers.py b/rxnutils/routes/readers.py index 033d265..308adfb 100644 --- a/rxnutils/routes/readers.py +++ b/rxnutils/routes/readers.py @@ -1,13 +1,14 @@ """Routines for reading routes from various formats""" + import copy -from typing import Sequence, List, Dict, Any +from typing import Any, Dict, List, Sequence import pandas as pd from rdkit import Chem +from rdkit.Chem import AllChem -from rxnutils.routes.base import SynthesisRoute -from rxnutils.routes.base import smiles2inchikey -from rxnutils.chem.utils import split_smiles_from_reaction, join_smiles_from_reaction +from rxnutils.chem.utils import join_smiles_from_reaction, split_smiles_from_reaction +from rxnutils.routes.base import SynthesisRoute, smiles2inchikey def read_reaction_lists(filename: str) -> List[SynthesisRoute]: @@ -169,9 +170,44 @@ def make_dict(product_smiles): return SynthesisRoute(make_dict(inchi_map[target_inchi])) +def read_rdf_file(filename: str) -> SynthesisRoute: + def finish_reaction(): + if not rxnblock: + return + rxn = AllChem.ReactionFromRxnBlock( + "\n".join(rxnblock), sanitize=False, strictParsing=False + ) + reactions.append(AllChem.ReactionToSmiles(rxn)) + + with open(filename, "r") as fileobj: + lines = fileobj.read().splitlines() + + reactions = [] + rxnblock = [] + read_rxn = skip_entry = False + for line in lines: + if line.startswith("$RFMT"): + read_rxn = skip_entry = False + finish_reaction() + rxnblock = [] + elif line.startswith(("$MFMT", "$DATUM")): + # Ignore MFMT and DATUM entries for now + skip_entry = True + elif skip_entry: + continue + elif line.startswith("$RXN"): + rxnblock = [line] + read_rxn = True + elif read_rxn: + rxnblock.append(line) + finish_reaction() + + return reactions2route(reactions) + + def _transform_retrosynthesis_atom_mapping(tree_dict: Dict[str, Any]) -> None: """ - Routes output from AiZynth have atom-mapping from the template-based model, + Routes output from AiZynth has atom-mapping from the template-based model, but it needs to be processed 1. Remove atom-mapping from reactants not in product 2. Reverse reaction SMILES diff --git a/rxnutils/routes/retro_bleu/__init__.py b/rxnutils/routes/retro_bleu/__init__.py new file mode 100644 index 0000000..5946c1d --- /dev/null +++ b/rxnutils/routes/retro_bleu/__init__.py @@ -0,0 +1,4 @@ +from rxnutils.routes.retro_bleu.scoring import ( + ngram_overlap_score, # noqa + retro_bleu_score, +) diff --git a/rxnutils/routes/retro_bleu/ngram_collection.py b/rxnutils/routes/retro_bleu/ngram_collection.py new file mode 100644 index 0000000..bb0d3a1 --- /dev/null +++ b/rxnutils/routes/retro_bleu/ngram_collection.py @@ -0,0 +1,111 @@ +""" +Contains routines for creating, reading, and writing n-gram collections + +Can be run as a module to create a collection from a set of routes: + + python -m rxnutils.routes.retro_bleu.ngram_collection --filename routes.json --output ngrams.json --nitems 2 --metadata template_hash + +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any, Set, Tuple + +from tqdm import tqdm + +from rxnutils.routes.base import SynthesisRoute + + +@dataclass +class NgramCollection: + """ + Class to create, read, and write a collection of n-grams + + :param nitems: the length of each n-gram + :param metadata_key: the key used to extract the n-grams from the reactions + :param ngrams: the extracted n-grams + """ + + nitems: int + metadata_key: str + ngrams: Set[Tuple[str, ...]] + + @classmethod + def from_file(cls, filename: str) -> "NgramCollection": + """ + Read an n-gram collection from a JSON-file + + :param filename: the path to the file + :return: the n-gram collection + """ + with open(filename, "r") as fileobj: + dict_ = json.load(fileobj) + ngrams = {tuple(item.split("\t")) for item in dict_["ngrams"]} + return NgramCollection(dict_["nitems"], dict_["metadata_key"], ngrams) + + @classmethod + def from_tree_collection( + cls, filename: str, nitems: int, metadata_key: str + ) -> "NgramCollection": + """ + Make a n-gram collection by extracting them from a collection of + synthesis routes. + + :param filename: the path to a file with a list of synthesis routes + :param nitems: the length of the gram + :param metadata_key: the metadata to extract + :return: the n-gram collection + """ + with open(filename, "r") as fileobj: + tree_list = json.load(fileobj) + + ngrams = set() + for tree_dict in tqdm(tree_list, leave=False): + route = SynthesisRoute(tree_dict) + ngrams = ngrams.union(route.reaction_ngrams(nitems, metadata_key)) + return NgramCollection(nitems, metadata_key, ngrams) + + def save_to_file(self, filename: str) -> None: + """ + Save an n-gram collection to a JSON-file + + :param filename: the path to the file + """ + dict_ = { + "nitems": self.nitems, + "metadata_key": self.metadata_key, + "ngrams": ["\t".join(ngram) for ngram in self.ngrams], + } + with open(filename, "w") as fileobj: + json.dump(dict_, fileobj) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser( + "Tool for making n-gram collections from a set of synthesis routes" + ) + parser.add_argument( + "--filename", nargs="+", help="the path to the synthesis routes" + ) + parser.add_argument("--output", help="the path to the n-gram collection") + parser.add_argument("--nitems", type=int, help="the length of the gram") + parser.add_argument( + "--metadata", help="the reaction metadata to extract for making the n-grams" + ) + args = parser.parse_args() + + collection = None + for filename in args.filename: + temp = NgramCollection.from_tree_collection( + filename, args.nitems, args.metadata + ) + if collection is None: + collection = temp + else: + collection.ngrams = collection.ngrams.union(temp.ngrams) + print(f"Collected unique {len(collection.ngrams)} {args.nitems}-grams") + collection.save_to_file(args.output) diff --git a/rxnutils/routes/retro_bleu/scoring.py b/rxnutils/routes/retro_bleu/scoring.py new file mode 100644 index 0000000..72f6921 --- /dev/null +++ b/rxnutils/routes/retro_bleu/scoring.py @@ -0,0 +1,48 @@ +""" Contains routine to score routes according to Retro-BLEU paper +""" + +import numpy as np + +from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.retro_bleu.ngram_collection import NgramCollection + + +def ngram_overlap_score( + route: SynthesisRoute, + ref: NgramCollection, +) -> float: + """ + Calculate the fractional n-gram overlap of the n-grams in the given + route and the reference n-gram collection + + :param route: the route to score + :param ref: the reference n-gram collection + :return: the calculated score + """ + route_ngrams = set(route.reaction_ngrams(ref.nitems, ref.metadata_key)) + if not route_ngrams: + return np.nan + return len(route_ngrams.intersection(ref.ngrams)) / len(route_ngrams) + + +def retro_bleu_score( + route: SynthesisRoute, ref: NgramCollection, ideal_steps: int = 3 +) -> float: + """ + Calculate the Retro-BLEU score according to the paper: + + Li, Junren, Lei Fang, och Jian-Guang Lou. + ”Retro-BLEU: quantifying chemical plausibility of retrosynthesis routes through reaction template sequence analysis”. + Digital Discovery 3, nr 3 (2024): 482–90. https://doi.org/10.1039/D3DD00219E. + + :param route: the route to score + :param ref: the reference n-gram collection + :param ideal_steps: a length-penalization hyperparameter (see Eq 2 in ref) + :return: the calculated score + """ + overlap = ngram_overlap_score(route, ref) + + nreactions = len(route.reaction_smiles()) + heuristic_score = ideal_steps / max(nreactions, ideal_steps) + + return np.exp(overlap) + np.exp(heuristic_score) diff --git a/rxnutils/routes/scoring.py b/rxnutils/routes/scoring.py new file mode 100644 index 0000000..1c16bb4 --- /dev/null +++ b/rxnutils/routes/scoring.py @@ -0,0 +1,86 @@ +""" Routines for scoring synthesis routes +""" + +from typing import Any, Callable, Dict, List, Tuple + +import numpy as np + +from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.retro_bleu.scoring import ( + ngram_overlap_score, # noqa + retro_bleu_score, +) + + +def route_sorter( + routes: List[SynthesisRoute], scorer: Callable[..., float], **kwargs: Any +) -> Tuple[List[SynthesisRoute], List[float]]: + """ + Scores and sort a list of routes. + Returns a tuple of the sorted routes and their scores. + + :param routes: the routes to score + :param scorer: the scorer function + :param kwargs: additional argument given to the scorer + :return: the sorted routes and their scores + """ + scores = np.asarray([scorer(route, **kwargs) for route in routes]) + sorted_idx = np.argsort(scores) + routes = [routes[idx] for idx in sorted_idx] + return routes, scores[sorted_idx].tolist() + + +def route_ranks(scores: List[float]) -> List[int]: + """ + Compute the rank of route scores. Rank starts at 1 + + :param scores: the route scores + :return: a list of ranks for each route + """ + ranks = [1] + for idx in range(1, len(scores)): + if abs(scores[idx] - scores[idx - 1]) < 1e-8: + ranks.append(ranks[idx - 1]) + else: + ranks.append(ranks[idx - 1] + 1) + return ranks + + +def badowski_route_score( + route: SynthesisRoute, + mol_costs: Dict[bool, float] = None, + average_yield: float = 0.8, + reaction_cost: float = 1.0, +) -> float: + """ + Calculate the score of route using the method from + (Badowski et al. Chem Sci. 2019, 10, 4640). + + The reaction cost is constant and the yield is an average yield. + The starting materials are assigned a cost based on whether they are in + stock or not. By default starting material in stock is assigned a + cost of 1 and starting material not in stock is assigned a cost of 10. + + To be accurate, each molecule node need to have an extra + boolean property called `in_stock`. + + :param route: the route to analyze + :param mol_costs: the starting material cost + :param average_yield: the average yield, defaults to 0.8 + :param reaction_cost: the reaction cost, defaults to 1.0 + :return: the computed cost + """ + + def traverse(tree_dict): + mol_cost = mol_costs or {True: 1, False: 10} + + reactions = tree_dict.get("children", []) + if not reactions: + return mol_cost[tree_dict.get("in_stock", True)] + + child_sum = sum( + 1 / average_yield * traverse(child) for child in reactions[0]["children"] + ) + return reaction_cost + child_sum + + return traverse(route.reaction_tree) diff --git a/rxnutils/routes/ted/__init__.py b/rxnutils/routes/ted/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rxnutils/routes/ted/distances_calculator.py b/rxnutils/routes/ted/distances_calculator.py new file mode 100644 index 0000000..338a699 --- /dev/null +++ b/rxnutils/routes/ted/distances_calculator.py @@ -0,0 +1,35 @@ +""" Module contain method to compute distance matrix using TED """ +import time +from typing import Sequence + +import numpy as np + +from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.ted.reactiontree import ReactionTreeWrapper + + +def ted_distances_calculator( + routes: Sequence[SynthesisRoute], content: str = "both", timeout: int = None +) -> np.ndarray: + """ + Compute the TED distances between each pair of routes + + :param routes: the routes to calculate pairwise distance on + :param content: determine what part of the synthesis trees to include in the calculation, + options 'molecules', 'reactions' and 'both', default 'both' + :param timeout: if given, raises an exception if timeout is taking longer time + :return: the square distance matrix + """ + distances = np.zeros([len(routes), len(routes)]) + distance_wrappers = [ReactionTreeWrapper(route, content) for route in routes] + time0 = time.perf_counter() + for i, iwrapper in enumerate(distance_wrappers): + # fmt: off + for j, jwrapper in enumerate(distance_wrappers[i + 1:], i + 1): + distances[i, j] = iwrapper.distance_to(jwrapper) + distances[j, i] = distances[i, j] + # fmt: on + time_past = time.perf_counter() - time0 + if timeout is not None and time_past > timeout: + raise ValueError(f"Unable to compute distance matrix in {timeout} s") + return distances diff --git a/rxnutils/routes/ted/reactiontree.py b/rxnutils/routes/ted/reactiontree.py new file mode 100644 index 0000000..91ab3aa --- /dev/null +++ b/rxnutils/routes/ted/reactiontree.py @@ -0,0 +1,301 @@ +""" +Module containing helper classes to compute the distance between to reaction trees using the APTED method +Since APTED is based on ordered trees and the reaction trees are unordered, plenty of +heuristics are implemented to deal with this. +""" +from __future__ import annotations + +import itertools +import math +from copy import deepcopy +from logging import getLogger +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union + +import numpy as np +from apted import APTED as Apted + +from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.ted.utils import ( + AptedConfig, + StandardFingerprintFactory, + TreeContent, +) + +StrDict = Dict[str, Any] +_FloatIterator = Iterable[float] + + +class ReactionTreeWrapper: + """ + Wrapper for a reaction tree that can calculate distances between + trees. + + :param route: the synthesis route to wrap + :param content: the content of the route to consider in the distance calculation, default 'molecules' + :param exhaustive_limit: if the number of possible ordered trees are below this limit create them all, default 20 + :param fp_factory: the factory of the fingerprint, Morgan fingerprint for molecules and reactions by default + :param dist_func: the distance function to use when renaming nodes + """ + + _index_permutations = { + n: list(itertools.permutations(range(n), n)) for n in range(1, 8) + } + + def __init__( + self, + route: SynthesisRoute, + content: Union[str, TreeContent] = TreeContent.MOLECULES, + exhaustive_limit: int = 20, + fp_factory: Callable[[StrDict, Optional[StrDict]], None] = None, + dist_func: Callable[[np.ndarray, np.ndarray], float] = None, + ) -> None: + single_node_tree = not bool(route.reaction_smiles()) + if single_node_tree and content == TreeContent.REACTIONS: + raise ValueError( + "Cannot create wrapping with content = reactions for a tree without reactions" + ) + + self._logger = getLogger() + # Will convert string input automatically + self._content = TreeContent(content) + self._base_tree = deepcopy(route.reaction_tree) + + self._fp_factory = fp_factory or StandardFingerprintFactory() + self._add_fingerprints(self._base_tree) + + if self._content != TreeContent.MOLECULES and not single_node_tree: + self._add_fingerprints(self._base_tree["children"][0], self._base_tree) + + if self._content == TreeContent.MOLECULES: + self._base_tree = self._remove_children_nodes(self._base_tree) + elif not single_node_tree and self._content == TreeContent.REACTIONS: + self._base_tree = self._remove_children_nodes( + self._base_tree["children"][0] + ) + + self._trees = [] + self._tree_count, self._node_index_list = self._inspect_tree() + self._enumeration = self._tree_count <= exhaustive_limit + + if self._enumeration: + self._create_all_trees() + else: + self._trees.append(self._base_tree) + + self._dist_func = dist_func + + @property + def info(self) -> StrDict: + """Return a dictionary with internal information about the wrapper""" + return { + "content": self._content, + "tree count": self._tree_count, + "enumeration": self._enumeration, + } + + @property + def first_tree(self) -> StrDict: + """Return the first created ordered tree""" + return self._trees[0] + + @property + def trees(self) -> List[StrDict]: + """Return a list of all created ordered trees""" + return self._trees + + def distance_iter( + self, other: "ReactionTreeWrapper", exhaustive_limit: int = 20 + ) -> _FloatIterator: + """ + Iterate over all distances computed between this and another tree + + There are three possible enumeration of distances possible dependent + on the number of possible ordered trees for the two routes that are compared + + * If the product of the number of possible ordered trees for both routes are + below `exhaustive_limit` compute the distance between all pair of trees + * If both self and other has been fully enumerated (i.e. all ordered trees has been created) + compute the distances between all trees of the route with the most ordered trees and + the first tree of the other route + * Compute `exhaustive_limit` number of distances by shuffling the child order for + each of the routes. + + The rules are applied top-to-bottom. + + :param other: another tree to calculate distance to + :param exhaustive_limit: used to determine what type of enumeration to do + :yield: the next computed distance between self and other + """ + if self._tree_count * other.info["tree count"] < exhaustive_limit: + yield from self._distance_iter_exhaustive(other) + elif self._enumeration or other.info["enumeration"]: + yield from self._distance_iter_semi_exhaustive(other) + else: + yield from self._distance_iter_random(other, exhaustive_limit) + + def distance_to( + self, other: "ReactionTreeWrapper", exhaustive_limit: int = 20 + ) -> float: + """ + Calculate the minimum distance from this route to another route + + Enumerate the distances using `distance_iter`. + + :param other: another tree to calculate distance to + :param exhaustive_limit: used to determine what type of enumeration to do + :return: the minimum distance + """ + min_dist = 1e6 + min_iter = -1 + for iteration, distance in enumerate( + self.distance_iter(other, exhaustive_limit) + ): + if distance < min_dist: + min_iter = iteration + min_dist = distance + self._logger.debug(f"Found minimum after {min_iter} iterations") + return min_dist + + def distance_to_with_sorting(self, other: "ReactionTreeWrapper") -> float: + """ + Compute the distance to another tree, by simpling sorting the children + of both trees. This is not guaranteed to return the minimum distance. + + :param other: another tree to calculate distance to + :return: the distance + """ + config = AptedConfig(sort_children=True, dist_func=self._dist_func) + return Apted(self.first_tree, other.first_tree, config).compute_edit_distance() + + def _add_fingerprints(self, tree: StrDict, parent: StrDict = None) -> None: + if "fingerprint" not in tree: + try: + self._fp_factory(tree, parent) + except ValueError: + pass + if "fingerprint" not in tree: + tree["fingerprint"] = [] + tree["sort_key"] = "".join(f"{digit}" for digit in tree["fingerprint"]) + if "children" not in tree: + tree["children"] = [] + + for child in tree["children"]: + for grandchild in child["children"]: + self._add_fingerprints(grandchild, child) + + def _create_all_trees(self) -> None: + self._trees = [] + # Iterate over all possible combinations of child order + for order_list in itertools.product(*self._node_index_list): + self._trees.append( + self._create_tree_recursively(self._base_tree, list(order_list)) + ) + + def _create_tree_recursively( + self, + node: StrDict, + order_list: List[List[int]], + ) -> StrDict: + new_tree = self._make_base_copy(node) + children = node.get("children", []) + if children: + child_order = order_list.pop(0) + assert len(child_order) == len(children) + new_children = [ + self._create_tree_recursively(child, order_list) for child in children + ] + new_tree["children"] = [new_children[idx] for idx in child_order] + return new_tree + + def _distance_iter_exhaustive(self, other: "ReactionTreeWrapper") -> _FloatIterator: + self._logger.debug( + f"APTED: Exhaustive search. {len(self.trees)} {len(other.trees)}" + ) + config = AptedConfig(randomize=False, dist_func=self._dist_func) + for tree1, tree2 in itertools.product(self.trees, other.trees): + yield Apted(tree1, tree2, config).compute_edit_distance() + + def _distance_iter_random( + self, other: "ReactionTreeWrapper", ntimes: int + ) -> _FloatIterator: + self._logger.debug( + f"APTED: Heuristic search. {len(self.trees)} {len(other.trees)}" + ) + config = AptedConfig(randomize=False, dist_func=self._dist_func) + yield Apted(self.first_tree, other.first_tree, config).compute_edit_distance() + + config = AptedConfig(randomize=True, dist_func=self._dist_func) + for _ in range(ntimes): + yield Apted( + self.first_tree, other.first_tree, config + ).compute_edit_distance() + + def _distance_iter_semi_exhaustive( + self, other: "ReactionTreeWrapper" + ) -> _FloatIterator: + self._logger.debug( + f"APTED: Semi-exhaustive search. {len(self.trees)} {len(other.trees)}" + ) + if len(self.trees) < len(other.trees): + first_wrapper = self + second_wrapper = other + else: + first_wrapper = other + second_wrapper = self + + config = AptedConfig(randomize=False, dist_func=self._dist_func) + for tree1 in first_wrapper.trees: + yield Apted( + tree1, second_wrapper.first_tree, config + ).compute_edit_distance() + + def _inspect_tree(self) -> Tuple[int, List[List[int]]]: + """ + Find the number of children for each node in the tree, which + will be used to compute the number of possible combinations of child orders + + Also accumulate the possible child orders for the nodes. + """ + + def _recurse_tree(node): + children = node.get("children", []) + nchildren = len(children) + permutations.append(math.factorial(nchildren)) + + if nchildren > 0: + node_index_list.append(list(self._index_permutations[nchildren])) + for child in children: + _recurse_tree(child) + + permutations: List[int] = [] + node_index_list: List[List[int]] = [] + _recurse_tree(self._base_tree) + if not permutations: + return 0, [] + return int(np.prod(permutations)), node_index_list + + @staticmethod + def _make_base_copy(node: StrDict) -> StrDict: + return { + "type": node["type"], + "smiles": node.get("smiles", ""), + "metadata": node.get("metadata"), + "fingerprint": node["fingerprint"], + "sort_key": node["sort_key"], + "children": [], + } + + @staticmethod + def _remove_children_nodes(tree: StrDict) -> StrDict: + new_tree = ReactionTreeWrapper._make_base_copy(tree) + + if tree.get("children"): + new_tree["children"] = [] + for child in tree["children"]: + new_tree["children"].extend( + [ + ReactionTreeWrapper._remove_children_nodes(grandchild) + for grandchild in child.get("children", []) + ] + ) + return new_tree diff --git a/rxnutils/routes/ted/utils.py b/rxnutils/routes/ted/utils.py new file mode 100644 index 0000000..81c3fc6 --- /dev/null +++ b/rxnutils/routes/ted/utils.py @@ -0,0 +1,110 @@ +""" Module containing utilities for TED calculations """ +from __future__ import annotations + +import random +from enum import Enum +from operator import itemgetter +from typing import Any, Callable, Dict, List + +import numpy as np +from apted import Config as BaseAptedConfig +from rdkit import Chem, DataStructs +from rdkit.Chem import AllChem +from scipy.spatial.distance import jaccard as jaccard_dist + +StrDict = Dict[str, Any] + + +class TreeContent(str, Enum): + """Possibilities for distance calculations on reaction trees""" + + MOLECULES = "molecules" + REACTIONS = "reactions" + BOTH = "both" + + +class AptedConfig(BaseAptedConfig): + """ + This is a helper class for the tree edit distance + calculation. It defines how the substitution + cost is calculated and how to obtain children nodes. + + :param randomize: if True, the children will be shuffled + :param sort_children: if True, the children will be sorted + :param dist_func: the distance function used for renaming nodes, Jaccard by default + """ + + def __init__( + self, + randomize: bool = False, + sort_children: bool = False, + dist_func: Callable[[np.ndarray, np.ndarray], float] = None, + ) -> None: + super().__init__() + self._randomize = randomize + self._sort_children = sort_children + self._dist_func = dist_func or jaccard_dist + + def rename(self, node1: StrDict, node2: StrDict) -> float: + if node1["type"] != node2["type"]: + return 1 + + fp1 = node1["fingerprint"] + fp2 = node2["fingerprint"] + return self._dist_func(fp1, fp2) + + def children(self, node: StrDict) -> List[StrDict]: + if self._sort_children: + return sorted(node["children"], key=itemgetter("sort_key")) + if not self._randomize: + return node["children"] + children = list(node["children"]) + random.shuffle(children) + return children + + +class StandardFingerprintFactory: + """ + Calculate Morgan fingerprint for molecules, and difference fingerprints for reactions + + :param radius: the radius of the fingerprint + :param nbits: the fingerprint lengths + """ + + def __init__(self, radius: int = 2, nbits: int = 2048) -> None: + self._fp_params = (radius, nbits) + + def __call__(self, tree: StrDict, parent: StrDict = None) -> None: + if tree["type"] == "reaction": + if parent is None: + raise ValueError( + "Must specify parent when making Morgan fingerprints for reaction nodes" + ) + self._add_rxn_fingerprint(tree, parent) + else: + self._add_mol_fingerprints(tree) + + def _add_mol_fingerprints(self, tree: StrDict) -> None: + if "fingerprint" not in tree: + mol = Chem.MolFromSmiles(tree["smiles"]) + rd_fp = AllChem.GetMorganFingerprintAsBitVect(mol, *self._fp_params) + tree["fingerprint"] = np.zeros((1,), dtype=np.int8) + DataStructs.ConvertToNumpyArray(rd_fp, tree["fingerprint"]) + tree["sort_key"] = "".join(f"{digit}" for digit in tree["fingerprint"]) + if "children" not in tree: + tree["children"] = [] + + for child in tree["children"]: + for grandchild in child["children"]: + self._add_mol_fingerprints(grandchild) + + def _add_rxn_fingerprint(self, node: StrDict, parent: StrDict) -> None: + if "fingerprint" not in node: + node["fingerprint"] = parent["fingerprint"].copy() + for reactant in node["children"]: + node["fingerprint"] -= reactant["fingerprint"] + node["sort_key"] = "".join(f"{digit}" for digit in node["fingerprint"]) + + for child in node["children"]: + for grandchild in child.get("children", []): + self._add_rxn_fingerprint(grandchild, child) diff --git a/rxnutils/routes/utils/__init__.py b/rxnutils/routes/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rxnutils/routes/utils/validation.py b/rxnutils/routes/utils/validation.py new file mode 100644 index 0000000..9f2905b --- /dev/null +++ b/rxnutils/routes/utils/validation.py @@ -0,0 +1,39 @@ +""" Module containing routes to validate AiZynthFinder-like input dictionaries """ +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, StringConstraints, ValidationError, conlist +from typing_extensions import Annotated + +StrDict = Dict[str, Any] + + +class ReactionNode(BaseModel): + """Node representing a reaction""" + + type: Annotated[str, StringConstraints(pattern=r"^reaction$")] + children: List[MoleculeNode] + + +class MoleculeNode(BaseModel): + """Node representing a molecule""" + + smiles: str + type: Annotated[str, StringConstraints(pattern=r"^mol$")] + children: Optional[conlist(ReactionNode, min_length=1, max_length=1)] = None + + +MoleculeNode.update_forward_refs() + + +def validate_dict(dict_: StrDict) -> None: + """ + Check that the route dictionary is a valid structure + + :param dict_: the route as dictionary + """ + try: + MoleculeNode(**dict_, extra="ignore") + except ValidationError as err: + raise ValueError(f"Invalid input: {err.json()}") diff --git a/tests/conftest.py b/tests/conftest.py index 8ae4692..9bb0e3e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,11 @@ import copy +import json import pandas as pd import pytest from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.readers import reactions2route @pytest.fixture @@ -66,6 +68,17 @@ def branched_synthesis_route(): return SynthesisRoute(dict_) +@pytest.fixture +def augmentable_sythesis_route(): + smiles0 = ["c1ccccc1>>Clc1ccccc1", "Cc1ccccc1>>c1ccccc1", "c1ccccc1O.C>>c1ccccc1C"] + metadata = [ + {"classification": "10.1.2 chlorination", "hash": "xyz"}, + {"hash": "xyy"}, + {"hash": "xxz"}, + ] + return reactions2route(smiles0, metadata) + + @pytest.fixture def setup_mapper(mocker, shared_datadir): df = pd.read_csv(shared_datadir / "mapped_tests_reactions.csv", sep="\t") @@ -73,3 +86,43 @@ def setup_mapper(mocker, shared_datadir): namerxn_mock.return_value.return_value = df namerxn_mock = mocker.patch("rxnutils.routes.base.RxnMapper") namerxn_mock.return_value.return_value = df + + +@pytest.fixture +def setup_mapper_no_namerxn(mocker, shared_datadir): + df = pd.read_csv(shared_datadir / "mapped_tests_reactions.csv", sep="\t") + namerxn_mock = mocker.patch("rxnutils.routes.base.NameRxn") + namerxn_mock.return_value.side_effect = FileNotFoundError("No namerxn") + namerxn_mock = mocker.patch("rxnutils.routes.base.RxnMapper") + df["NMC"] = "0.0" + namerxn_mock.return_value.return_value = df + + +@pytest.fixture +def load_reaction_tree(shared_datadir): + def wrapper(filename, index=0): + filename = str(shared_datadir / filename) + with open(filename, "r") as fileobj: + trees = json.load(fileobj) + if isinstance(trees, dict): + return trees + elif index == -1: + return trees + else: + return trees[index] + + return wrapper + + +@pytest.fixture +def setup_stock(): + def traverse(tree_dict, stock): + if tree_dict["type"] == "mol": + tree_dict["in_stock"] = tree_dict["smiles"] in stock + for child in tree_dict.get("children", []): + traverse(child, stock) + + def wrapper(route, stock): + traverse(route.reaction_tree, stock) + + return wrapper diff --git a/tests/data/atorvastatin_routes.json b/tests/data/atorvastatin_routes.json new file mode 100644 index 0000000..967dc44 --- /dev/null +++ b/tests/data/atorvastatin_routes.json @@ -0,0 +1,404 @@ +[ + { + "type": "mol", + "smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)O", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)N(c1ccccc1)c1ccccc1.O>>CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)O", + "tree_depth": 1, + "forward_step": 7, + "classification": "9.7.351", + "mapped_reaction_smiles": "[OH2:41].c1ccc(N(c2ccccc2)[C:39]([CH2:38][C@@H:36]([CH2:35][C@@H:33]([CH2:32][CH2:31][n:30]2[c:4]([CH:2]([CH3:1])[CH3:3])[c:5]([C:6](=[O:7])[NH:8][c:9]3[cH:10][cH:11][cH:12][cH:13][cH:14]3)[c:15](-[c:16]3[cH:17][cH:18][cH:19][cH:20][cH:21]3)[c:22]2-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[OH:34])[OH:37])=[O:40])cc1>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C@@H:36]([OH:37])[CH2:38][C:39](=[O:40])[OH:41]" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)N(c1ccccc1)c1ccccc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H]1C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)OC(C)(C)O1>>CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)N(c1ccccc1)c1ccccc1", + "tree_depth": 2, + "forward_step": 6, + "classification": "6.3.16", + "mapped_reaction_smiles": "CC1(C)[O:34][C@H:33]([CH2:32][CH2:31][n:30]2[c:4]([CH:2]([CH3:1])[CH3:3])[c:5]([C:6](=[O:7])[NH:8][c:9]3[cH:10][cH:11][cH:12][cH:13][cH:14]3)[c:15](-[c:16]3[cH:17][cH:18][cH:19][cH:20][cH:21]3)[c:22]2-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[CH2:35][C@H:36]([CH2:38][C:39](=[O:40])[N:142]([c:143]2[cH:144][cH:145][cH:146][cH:147][cH:148]2)[c:149]2[cH:150][cH:151][cH:152][cH:153][cH:154]2)[O:37]1>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C@@H:36]([OH:37])[CH2:38][C:39](=[O:40])[N:142]([c:143]1[cH:144][cH:145][cH:146][cH:147][cH:148]1)[c:149]1[cH:150][cH:151][cH:152][cH:153][cH:154]1" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H]1C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)OC(C)(C)O1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)C(=O)C(C(=O)Nc1ccccc1)C(C(=O)c1ccc(F)cc1)c1ccccc1.CC1(C)O[C@H](CCN)C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)O1>>CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H]1C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)OC(C)(C)O1", + "tree_depth": 3, + "forward_step": 5, + "classification": "4.1.14", + "mapped_reaction_smiles": "O=[C:4]([CH:2]([CH3:1])[CH3:3])[CH:5]([C:6](=[O:7])[NH:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1)[CH:15]([c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1)[C:22](=O)[c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1.[NH2:30][CH2:31][CH2:32][C@H:33]1[O:34][C:155]([CH3:156])([CH3:157])[O:37][C@@H:36]([CH2:38][C:39](=[O:40])[N:142]([c:143]2[cH:144][cH:145][cH:146][cH:147][cH:148]2)[c:149]2[cH:150][cH:151][cH:152][cH:153][cH:154]2)[CH2:35]1>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][C@H:33]1[O:34][C:155]([CH3:156])([CH3:157])[O:37][C@@H:36]([CH2:38][C:39](=[O:40])[N:142]([c:143]2[cH:144][cH:145][cH:146][cH:147][cH:148]2)[c:149]2[cH:150][cH:151][cH:152][cH:153][cH:154]2)[CH2:35]1" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)C(=O)C(C(=O)Nc1ccccc1)C(C(=O)c1ccc(F)cc1)c1ccccc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)C(=O)/C(=C/c1ccccc1)C(=O)Nc1ccccc1.O=Cc1ccc(F)cc1>>CC(C)C(=O)C(C(=O)Nc1ccccc1)C(C(=O)c1ccc(F)cc1)c1ccccc1", + "tree_depth": 4, + "forward_step": 4, + "classification": "0.0", + "mapped_reaction_smiles": "[CH3:1][CH:2]([CH3:3])[C:4](/[C:5]([C:6](=[O:7])[NH:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1)=[CH:15]/[c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1)=[O:158].[CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)=[O:159]>>[CH3:1][CH:2]([CH3:3])[C:4]([CH:5]([C:6](=[O:7])[NH:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1)[CH:15]([c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1)[C:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)=[O:159])=[O:158]" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)C(=O)/C(=C/c1ccccc1)C(=O)Nc1ccccc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)C(=O)CC(=O)Nc1ccccc1.O=Cc1ccccc1>>CC(C)C(=O)/C(=C/c1ccccc1)C(=O)Nc1ccccc1", + "tree_depth": 5, + "forward_step": 3, + "classification": "3.11.146", + "mapped_reaction_smiles": "O=[CH:15][c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1.[CH3:1][CH:2]([CH3:3])[C:4]([CH2:5][C:6](=[O:7])[NH:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1)=[O:158]>>[CH3:1][CH:2]([CH3:3])[C:4](/[C:5]([C:6](=[O:7])[NH:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1)=[CH:15]/[c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1)=[O:158]" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)C(=O)CC(=O)Nc1ccccc1" + }, + { + "type": "mol", + "smiles": "O=Cc1ccccc1" + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "O=Cc1ccc(F)cc1" + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "CC1(C)O[C@H](CCN)C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)O1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC1(C)O[C@H](CC#N)C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)O1>>CC1(C)O[C@H](CCN)C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)O1", + "tree_depth": 4, + "forward_step": 4, + "classification": "7.3.1", + "mapped_reaction_smiles": "[N:30]#[C:31][CH2:32][C@H:33]1[O:34][C:155]([CH3:156])([CH3:157])[O:37][C@@H:36]([CH2:38][C:39](=[O:40])[N:142]([c:143]2[cH:144][cH:145][cH:146][cH:147][cH:148]2)[c:149]2[cH:150][cH:151][cH:152][cH:153][cH:154]2)[CH2:35]1>>[NH2:30][CH2:31][CH2:32][C@H:33]1[O:34][C:155]([CH3:156])([CH3:157])[O:37][C@@H:36]([CH2:38][C:39](=[O:40])[N:142]([c:143]2[cH:144][cH:145][cH:146][cH:147][cH:148]2)[c:149]2[cH:150][cH:151][cH:152][cH:153][cH:154]2)[CH2:35]1" + }, + "children": [ + { + "type": "mol", + "smiles": "CC1(C)O[C@H](CC#N)C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)O1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "COC(C)(C)OC.N#CC[C@@H](O)C[C@@H](O)CC(=O)N(c1ccccc1)c1ccccc1>>CC1(C)O[C@H](CC#N)C[C@H](CC(=O)N(c2ccccc2)c2ccccc2)O1", + "tree_depth": 5, + "forward_step": 3, + "classification": "4.2.39", + "mapped_reaction_smiles": "CO[C:155](OC)([CH3:156])[CH3:157].[N:30]#[C:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C@@H:36]([OH:37])[CH2:38][C:39](=[O:40])[N:142]([c:143]1[cH:144][cH:145][cH:146][cH:147][cH:148]1)[c:149]1[cH:150][cH:151][cH:152][cH:153][cH:154]1>>[N:30]#[C:31][CH2:32][C@H:33]1[O:34][C:155]([CH3:156])([CH3:157])[O:37][C@@H:36]([CH2:38][C:39](=[O:40])[N:142]([c:143]2[cH:144][cH:145][cH:146][cH:147][cH:148]2)[c:149]2[cH:150][cH:151][cH:152][cH:153][cH:154]2)[CH2:35]1" + }, + "children": [ + { + "type": "mol", + "smiles": "COC(C)(C)OC" + }, + { + "type": "mol", + "smiles": "N#CC[C@@H](O)C[C@@H](O)CC(=O)N(c1ccccc1)c1ccccc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "N#CC[C@@H](O)CC(=O)CC(=O)N(c1ccccc1)c1ccccc1>>N#CC[C@@H](O)C[C@@H](O)CC(=O)N(c1ccccc1)c1ccccc1", + "tree_depth": 6, + "forward_step": 2, + "classification": "7.5.1", + "mapped_reaction_smiles": "[N:30]#[C:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C:36](=[O:37])[CH2:38][C:39](=[O:40])[N:142]([c:143]1[cH:144][cH:145][cH:146][cH:147][cH:148]1)[c:149]1[cH:150][cH:151][cH:152][cH:153][cH:154]1>>[N:30]#[C:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C@@H:36]([OH:37])[CH2:38][C:39](=[O:40])[N:142]([c:143]1[cH:144][cH:145][cH:146][cH:147][cH:148]1)[c:149]1[cH:150][cH:151][cH:152][cH:153][cH:154]1" + }, + "children": [ + { + "type": "mol", + "smiles": "N#CC[C@@H](O)CC(=O)CC(=O)N(c1ccccc1)c1ccccc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(=O)N(c1ccccc1)c1ccccc1.CCOC(=O)C[C@H](O)CC#N>>N#CC[C@@H](O)CC(=O)CC(=O)N(c1ccccc1)c1ccccc1", + "tree_depth": 7, + "forward_step": 1, + "classification": "3.11.93", + "mapped_reaction_smiles": "CCO[C:36]([CH2:35][C@@H:33]([CH2:32][C:31]#[N:30])[OH:34])=[O:37].[CH3:38][C:39](=[O:40])[N:142]([c:143]1[cH:144][cH:145][cH:146][cH:147][cH:148]1)[c:149]1[cH:150][cH:151][cH:152][cH:153][cH:154]1>>[N:30]#[C:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C:36](=[O:37])[CH2:38][C:39](=[O:40])[N:142]([c:143]1[cH:144][cH:145][cH:146][cH:147][cH:148]1)[c:149]1[cH:150][cH:151][cH:152][cH:153][cH:154]1" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(=O)N(c1ccccc1)c1ccccc1" + }, + { + "type": "mol", + "smiles": "CCOC(=O)C[C@H](O)CC#N" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "O" + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)O", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H]1C[C@@H](O)CC(=O)O1.O>>CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H](O)C[C@@H](O)CC(=O)O", + "tree_depth": 1, + "forward_step": 9, + "classification": "9.7.61", + "mapped_reaction_smiles": "[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][C@H:33]1[O:34][C:39](=[O:40])[CH2:38][C@H:36]([OH:37])[CH2:35]1.[OH2:41]>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C@@H:36]([OH:37])[CH2:38][C:39](=[O:40])[OH:41]" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H]1C[C@@H](O)CC(=O)O1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "COC(=O)CC(=O)C[C@H](O)CCn1c(-c2ccc(F)cc2)c(-c2ccccc2)c(C(=O)Nc2ccccc2)c1C(C)C>>CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CC[C@@H]1C[C@@H](O)CC(=O)O1", + "tree_depth": 2, + "forward_step": 8, + "classification": "0.0", + "mapped_reaction_smiles": "CO[C:39]([CH2:38][C:36]([CH2:35][C@@H:33]([CH2:32][CH2:31][n:30]1[c:4]([CH:2]([CH3:1])[CH3:3])[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22]1-[c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)[OH:34])=[O:37])=[O:40]>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][C@H:33]1[O:34][C:39](=[O:40])[CH2:38][C@H:36]([OH:37])[CH2:35]1" + }, + "children": [ + { + "type": "mol", + "smiles": "COC(=O)CC(=O)C[C@H](O)CCn1c(-c2ccc(F)cc2)c(-c2ccccc2)c(C(=O)Nc2ccccc2)c1C(C)C", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CCC=O.COC(=O)CC(C)=O>>COC(=O)CC(=O)C[C@H](O)CCn1c(-c2ccc(F)cc2)c(-c2ccccc2)c(C(=O)Nc2ccccc2)c1C(C)C", + "tree_depth": 3, + "forward_step": 7, + "classification": "3.11.1", + "mapped_reaction_smiles": "[CH3:35][C:36](=[O:37])[CH2:38][C:39](=[O:40])[O:143][CH3:142].[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][CH:33]=[O:34]>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][C@@H:33]([OH:34])[CH2:35][C:36](=[O:37])[CH2:38][C:39](=[O:40])[O:143][CH3:142]" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CCC=O", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CCC1OCCO1>>CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CCC=O", + "tree_depth": 4, + "forward_step": 6, + "classification": "6.5.2", + "mapped_reaction_smiles": "C1C[O:34][CH:33]([CH2:32][CH2:31][n:30]2[c:4]([CH:2]([CH3:1])[CH3:3])[c:5]([C:6](=[O:7])[NH:8][c:9]3[cH:10][cH:11][cH:12][cH:13][cH:14]3)[c:15](-[c:16]3[cH:17][cH:18][cH:19][cH:20][cH:21]3)[c:22]2-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)O1>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][CH:33]=[O:34]" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CCC1OCCO1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)C(=O)N(CCC1OCCO1)C(C(=O)O)c1ccc(F)cc1.O=C(C#Cc1ccccc1)Nc1ccccc1>>CC(C)c1c(C(=O)Nc2ccccc2)c(-c2ccccc2)c(-c2ccc(F)cc2)n1CCC1OCCO1", + "tree_depth": 5, + "forward_step": 5, + "classification": "0.0", + "mapped_reaction_smiles": "O=C(O)[CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)[N:30]([C:4](=O)[CH:2]([CH3:1])[CH3:3])[CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1.[C:5]([C:6](=[O:7])[NH:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1)#[C:15][c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1>>[CH3:1][CH:2]([CH3:3])[c:4]1[c:5]([C:6](=[O:7])[NH:8][c:9]2[cH:10][cH:11][cH:12][cH:13][cH:14]2)[c:15](-[c:16]2[cH:17][cH:18][cH:19][cH:20][cH:21]2)[c:22](-[c:23]2[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]2)[n:30]1[CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)C(=O)N(CCC1OCCO1)C(C(=O)O)c1ccc(F)cc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CCOC(=O)C(c1ccc(F)cc1)N(CCC1OCCO1)C(=O)C(C)C>>CC(C)C(=O)N(CCC1OCCO1)C(C(=O)O)c1ccc(F)cc1", + "tree_depth": 6, + "forward_step": 4, + "classification": "6.2.1", + "mapped_reaction_smiles": "CC[O:150][C:148]([CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)[N:30]([C:4]([CH:2]([CH3:1])[CH3:3])=[O:147])[CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1)=[O:149]>>[CH3:1][CH:2]([CH3:3])[C:4]([N:30]([CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)[C:148](=[O:149])[OH:150])[CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1)=[O:147]" + }, + "children": [ + { + "type": "mol", + "smiles": "CCOC(=O)C(c1ccc(F)cc1)N(CCC1OCCO1)C(=O)C(C)C", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CC(C)C(=O)Cl.CCOC(=O)C(NCCC1OCCO1)c1ccc(F)cc1>>CCOC(=O)C(c1ccc(F)cc1)N(CCC1OCCO1)C(=O)C(C)C", + "tree_depth": 7, + "forward_step": 3, + "classification": "2.1.1", + "mapped_reaction_smiles": "Cl[C:4]([CH:2]([CH3:1])[CH3:3])=[O:147].[CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)([NH:30][CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1)[C:148](=[O:149])[O:150][CH2:152][CH3:151]>>[CH3:1][CH:2]([CH3:3])[C:4]([N:30]([CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)[C:148](=[O:149])[O:150][CH2:152][CH3:151])[CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1)=[O:147]" + }, + "children": [ + { + "type": "mol", + "smiles": "CC(C)C(=O)Cl" + }, + { + "type": "mol", + "smiles": "CCOC(=O)C(NCCC1OCCO1)c1ccc(F)cc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CCOC(=O)C(Br)c1ccc(F)cc1.NCCC1OCCO1>>CCOC(=O)C(NCCC1OCCO1)c1ccc(F)cc1", + "tree_depth": 8, + "forward_step": 2, + "classification": "1.6.2", + "mapped_reaction_smiles": "Br[CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)[C:148](=[O:149])[O:150][CH2:152][CH3:151].[NH2:30][CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1>>[CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)([NH:30][CH2:31][CH2:32][CH:33]1[O:34][CH2:144][CH2:145][O:146]1)[C:148](=[O:149])[O:150][CH2:152][CH3:151]" + }, + "children": [ + { + "type": "mol", + "smiles": "CCOC(=O)C(Br)c1ccc(F)cc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "CCOC(=O)Cc1ccc(F)cc1.O=C1CCC(=O)N1Br>>CCOC(=O)C(Br)c1ccc(F)cc1", + "tree_depth": 9, + "forward_step": 1, + "classification": "10.1.5", + "mapped_reaction_smiles": "O=C1CCC(=O)N1[Br:153].[CH2:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)[C:148](=[O:149])[O:150][CH2:152][CH3:151]>>[CH:22]([c:23]1[cH:24][cH:25][c:26]([F:27])[cH:28][cH:29]1)([C:148](=[O:149])[O:150][CH2:152][CH3:151])[Br:153]" + }, + "children": [ + { + "type": "mol", + "smiles": "CCOC(=O)Cc1ccc(F)cc1" + }, + { + "type": "mol", + "smiles": "O=C1CCC(=O)N1Br" + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "NCCC1OCCO1" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "O=C(C#Cc1ccccc1)Nc1ccccc1", + "children": [ + { + "type": "reaction", + "metadata": { + "reaction_smiles": "Nc1ccccc1.O=C(O)C#Cc1ccccc1>>O=C(C#Cc1ccccc1)Nc1ccccc1", + "tree_depth": 6, + "forward_step": 4, + "classification": "2.1.2", + "mapped_reaction_smiles": "O[C:6]([C:5]#[C:15][c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1)=[O:7].[NH2:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1>>[C:5]([C:6](=[O:7])[NH:8][c:9]1[cH:10][cH:11][cH:12][cH:13][cH:14]1)#[C:15][c:16]1[cH:17][cH:18][cH:19][cH:20][cH:21]1" + }, + "children": [ + { + "type": "mol", + "smiles": "Nc1ccccc1" + }, + { + "type": "mol", + "smiles": "O=C(O)C#Cc1ccccc1" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "COC(=O)CC(C)=O" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "mol", + "smiles": "O" + } + ] + } + ] + } +] \ No newline at end of file diff --git a/tests/data/example_reactions_ids.csv b/tests/data/example_reactions_ids.csv new file mode 100644 index 0000000..6ce24ea --- /dev/null +++ b/tests/data/example_reactions_ids.csv @@ -0,0 +1 @@ +EN12797-88 diff --git a/tests/data/example_route.rdf b/tests/data/example_route.rdf new file mode 100644 index 0000000..ee7888c --- /dev/null +++ b/tests/data/example_route.rdf @@ -0,0 +1,68 @@ +$RDFILE 1 +$DATM 05.22.2024 09:48:44 +$RFMT $RIREG 1 +$RXN + + RDKit + + 1 1 +$MOL +undefined + RDKit 2D + + 4 3 0 0 0 0 0 0 0 0999 V2000 + 0.0007 0.0004 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.3000 0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.8993 0.7496 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 + 2 3 1 0 + 3 4 2 0 +M END +$MOL +undefined + RDKit 2D + + 5 4 0 0 0 0 0 0 0 0999 V2000 + 0.0007 0.0004 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.3000 0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.8993 0.7496 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6000 -1.5000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 + 2 3 1 0 + 3 4 2 0 + 3 5 1 0 +M END +$RFMT $RIREG 2 +$RXN + + RDKit + + 1 1 +$MOL +undefined + RDKit 2D + + 4 3 0 0 0 0 0 0 0 0999 V2000 + 0.0007 0.0004 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.3000 0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.8993 0.7496 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 + 2 3 1 0 + 3 4 1 0 +M END +$MOL +undefined + RDKit 2D + + 4 3 0 0 0 0 0 0 0 0999 V2000 + 0.0007 0.0004 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.3000 0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.8993 0.7496 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 + 2 3 1 0 + 3 4 2 0 +M END \ No newline at end of file diff --git a/tests/data/example_routes.json b/tests/data/example_routes.json new file mode 100644 index 0000000..4de5b7b --- /dev/null +++ b/tests/data/example_routes.json @@ -0,0 +1,148 @@ +[ + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(Cl)c2c1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "Nc1ccc(NC(=S)Nc2ccccc2)cc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[c:1]1([N:7][cH3:8])[cH:2][cH:3]:[N:4]:[cH:5][cH:6]1>>Cl[c:1]1[cH:2][cH:3]:[N:4]:[cH:5][cH:6]1.[N:7][cH3:8]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(Nc3ccc(NC(=S)Nc4ccccc4)cc3)c2c1", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(Cl)c2c1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "Nc1ccc(N)cc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[c:1]1([N:7][cH3:8])[cH:2][cH:3]:[N:4]:[cH:5][cH:6]1>>Cl[c:1]1[cH:2][cH:3]:[N:4]:[cH:5][cH:6]1.[N:7][cH3:8]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(Nc3ccc(N)cc3)c2c1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "S=C=Nc1ccccc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[N:1]([cH3:2])[C:4](=[S:3])[N:5][cH3:6]>>[N:1][cH3:2].[S:3]=[C:4]=[N:5][cH3:6]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(Nc3ccc(NC(=S)Nc4ccccc4)cc3)c2c1", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(N)c2c1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "Nc1ccc(Br)cc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[c:1]([cH2:2])([cH2:3])[N:4][cH3:5]>>Br[c:1]([cH2:2])[cH2:3].[N:4][cH3:5]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(Nc3ccc(N)cc3)c2c1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "S=C=Nc1ccccc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[N:1]([cH3:2])[C:4](=[S:3])[N:5][cH3:6]>>[N:1][cH3:2].[S:3]=[C:4]=[N:5][cH3:6]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "Cc1ccc2nc3ccccc3c(Nc3ccc(NC(=S)Nc4ccccc4)cc3)c2c1", + "type": "mol" + } +] diff --git a/tests/data/longer_routes.json b/tests/data/longer_routes.json new file mode 100644 index 0000000..8fcc8c8 --- /dev/null +++ b/tests/data/longer_routes.json @@ -0,0 +1,327 @@ +[ + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "CO", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "Fc1ccc(CBr)cc1", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "CC(C)(C)OC(N)=O", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "S=C(Cl)Cl", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1](=[S:2])=[N:4][CH3:3]>>Cl[C:1](Cl)=[S:2].[CH3:3][N:4]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "O=c1[nH]c(=O)[nH]c(=NCCN=C=S)[nH]1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[CH3:1][N:2][C:3](=[S:4])[N:6][CH3:5]>>[CH3:1][N:2]=[C:3]=[S:4].[CH3:5][N:6]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "CC(C)(C)OC(=O)NC(=S)NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "N", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1]([NH2:2])(=[N:3][CH3:4])[N:5]>>S=[C:1]([NH2:2])[N:3][CH3:4].[N:5]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "CC(C)(C)OC(=O)NC(N)=NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[N:1][CH3:2]>>CC(C)(C)OC(=O)[N:1][CH3:2]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "NC(N)=NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "O=[N+]([O-])c1ccc(CBr)cc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1]([cH3:2])[n:5]([cH:4]:[NH:3])[cH2:6]>>Br[C:1][cH3:2].[NH:3]:[cH:4][n:5][cH2:6]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "NC(N)=NCCN=c1[nH]c(=O)n(Cc2ccc([N+](=O)[O-])cc2)c(=O)[nH]1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1]([cH3:2])[n:5]([cH:4]:[NH:3])[cH:6]:[NH:7]>>Br[C:1][cH3:2].[NH:3]:[cH:4][n:5][cH:6]:[NH:7]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "NC(N)=NCCN=c1[nH]c(=O)n(Cc2ccc([N+](=O)[O-])cc2)c(=O)n1Cc1ccc(F)cc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[c:1]([cH2:2])([cH2:3])[O:5][CH3:4]>>F[c:1]([cH2:2])[cH2:3].[CH3:4][O:5]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "COc1ccc(Cn2c(=NCCN=C(N)N)[nH]c(=O)n(Cc3ccc([N+](=O)[O-])cc3)c2=O)cc1", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "COc1ccc(CCl)cc1", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "CC(C)(C)OC(N)=O", + "type": "mol" + }, + { + "children": [ + { + "children": [ + { + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "S=C(Cl)Cl", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1](=[S:2])=[N:4][CH3:3]>>Cl[C:1](Cl)=[S:2].[CH3:3][N:4]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "O=c1[nH]c(=O)[nH]c(=NCCN=C=S)[nH]1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[CH3:1][N:2][C:3](=[S:4])[N:6][CH3:5]>>[CH3:1][N:2]=[C:3]=[S:4].[CH3:5][N:6]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "CC(C)(C)OC(=O)NC(=S)NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "N", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1]([NH2:2])(=[N:3][CH3:4])[N:5]>>S=[C:1]([NH2:2])[N:3][CH3:4].[N:5]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "CC(C)(C)OC(=O)NC(N)=NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[N:1][CH3:2]>>CC(C)(C)OC(=O)[N:1][CH3:2]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "NC(N)=NCCN=c1[nH]c(=O)[nH]c(=O)[nH]1", + "type": "mol" + }, + { + "hide": false, + "in_stock": true, + "is_chemical": true, + "smiles": "O=[N+]([O-])c1ccc(CBr)cc1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1]([cH3:2])[n:4]([cH2:3])[cH2:5]>>Br[C:1][cH3:2].[cH2:3][n:4][cH2:5]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "NC(N)=NCCN=c1[nH]c(=O)n(Cc2ccc([N+](=O)[O-])cc2)c(=O)[nH]1", + "type": "mol" + } + ], + "hide": false, + "is_reaction": true, + "metadata": {}, + "smiles": "[C:1]([cH3:2])[n:5]([cH:4]:[NH:3])[cH2:6]>>Cl[C:1][cH3:2].[NH:3]:[cH:4][n:5][cH2:6]", + "type": "reaction" + } + ], + "hide": false, + "in_stock": false, + "is_chemical": true, + "smiles": "COc1ccc(Cn2c(=NCCN=C(N)N)[nH]c(=O)n(Cc3ccc([N+](=O)[O-])cc3)c2=O)cc1", + "type": "mol" + } +] diff --git a/tests/test_chem_utils.py b/tests/test_chem_utils.py index ce3a7c0..22e9e41 100644 --- a/tests/test_chem_utils.py +++ b/tests/test_chem_utils.py @@ -14,6 +14,7 @@ get_special_groups, reaction_centres, ) +from rxnutils.chem.augmentation import single_reactant_augmentation @pytest.mark.parametrize( @@ -140,3 +141,37 @@ def test_reaction_centers(smiles, expected): rxn.Initialize() assert reaction_centres(rxn) == expected + + +@pytest.mark.parametrize( + ("smiles", "classification", "expected"), + [ + ( + "A.B>>C", + "", + "A.B>>C", + ), + ( + "A.B>>C", + "0.0", + "A.B>>C", + ), + ( + "A>>C", + "0.0", + "A>>C", + ), + ( + "A>>C", + "10.1.1", + "Br.A>>C", + ), + ( + "A>>C", + "10.1.2 Chlorination", + "Cl.A>>C", + ), + ], +) +def test_single_reactant_agumentation(smiles, classification, expected): + assert single_reactant_augmentation(smiles, classification) == expected diff --git a/tests/test_product_tagging.py b/tests/test_product_tagging.py index 71577f9..14c2339 100644 --- a/tests/test_product_tagging.py +++ b/tests/test_product_tagging.py @@ -1,6 +1,7 @@ import pytest from rxnutils.chem.disconnection_sites.atom_map_tagging import ( atom_map_tag_products, + atom_map_tag_reactants, get_atom_list, ) from rxnutils.chem.disconnection_sites.tag_converting import ( @@ -49,6 +50,26 @@ def test_atom_map_tag_products(reactants_smiles, product_smiles, expected): assert tagged_product == expected +@pytest.mark.parametrize( + ("reactants_smiles", "product_smiles", "expected"), + [ + ( + "[Cl:2].[CH:1]1=[CH:7][CH:6]=[CH:5][CH:4]=[CH:3]1", + "[Cl:2][C:1]1=[CH:7][CH:6]=[CH:5][CH:4]=[CH:3]1", + "[Cl:1].c1cc[cH:1]cc1", + ), + ( + "Cl.[CH:1]1=[CH:7][CH:6]=[CH:5][CH:4]=[CH:3]1", + "Cl[C:1]1=[CH:7][CH:6]=[CH:5][CH:4]=[CH:3]1", + "Cl.c1cc[cH:1]cc1", + ), + ], +) +def test_atom_map_tag_reactants(reactants_smiles, product_smiles, expected): + tagged_reactants = atom_map_tag_reactants(f"{reactants_smiles}>>{product_smiles}") + assert tagged_reactants == expected + + @pytest.mark.parametrize( ("product_smiles", "expected"), [ diff --git a/tests/test_routes_base.py b/tests/test_routes_base.py index 960a631..a9b02d2 100644 --- a/tests/test_routes_base.py +++ b/tests/test_routes_base.py @@ -9,10 +9,22 @@ def test_collect_reaction_smiles(synthesis_route): smiles = synthesis_route.reaction_smiles() + assert synthesis_route.nsteps == 2 assert len(smiles) == 2 assert smiles[0] == "CO.Clc1ccccc1>>COc1ccccc1" +def test_collect_reaction_smiles_augmented(augmentable_sythesis_route): + smiles = augmentable_sythesis_route.reaction_smiles(augment=True) + + assert len(smiles) == 3 + assert smiles == [ + "Cl.c1ccccc1>>Clc1ccccc1", + "Cc1ccccc1>>c1ccccc1", + "Oc1ccccc1.C>>Cc1ccccc1", + ] + + def test_atom_mapping(synthesis_route, setup_mapper): synthesis_route.assign_atom_mapping() @@ -33,6 +45,39 @@ def test_atom_mapping(synthesis_route, setup_mapper): ) +def test_atom_mapping_no_namerxn(synthesis_route, setup_mapper_no_namerxn, recwarn): + synthesis_route.assign_atom_mapping() + + assert len(recwarn) == 1 + assert "namerxn" in str(recwarn[0]) + + data = pd.DataFrame(synthesis_route.reaction_data()) + + assert len(data) == 2 + assert data["classification"].to_list() == ["0.0", "0.0"] + assert ( + data["mapped_reaction_smiles"][0] + == "Cl[c:3]1[cH:4][cH:5][cH:6][cH:7][cH:8]1.[CH3:1][OH:2]>>[CH3:1][O:2][c:3]1[cH:4][cH:5][cH:6][cH:7][cH:8]1" + ) + + +def test_atom_mapping_no_namerxn_choose_rxnmapper( + synthesis_route, setup_mapper_no_namerxn, recwarn +): + synthesis_route.assign_atom_mapping(only_rxnmapper=True) + + assert len(recwarn) == 0 + + data = pd.DataFrame(synthesis_route.reaction_data()) + + assert len(data) == 2 + assert data["classification"].to_list() == ["0.0", "0.0"] + assert ( + data["mapped_reaction_smiles"][0] + == "Cl[c:3]1[cH:4][cH:5][cH:6][cH:7][cH:8]1.[CH3:1][OH:2]>>[CH3:1][O:2][c:3]1[cH:4][cH:5][cH:6][cH:7][cH:8]1" + ) + + def test_root_smiles(synthesis_route, setup_mapper): with pytest.raises(ValueError): SynthesisRoute({"smiles": "C"}).mapped_root_smiles @@ -160,3 +205,58 @@ def test_extract_branched_chains(branched_synthesis_route): assert chains[1][2]["step"] == 2 assert chains[1][2]["chain"] == "sub1" assert chains[1][2]["type"] == "branch" + + +def test_route_leaves(synthesis_route): + leaves = synthesis_route.leaves() + + assert leaves == {"c1ccccc1", "Cl", "CO"} + + +def test_route_is_solved(synthesis_route, setup_stock): + assert synthesis_route.is_solved() + + setup_stock(synthesis_route, {"c1ccccc1", "Cl", "CO"}) + assert synthesis_route.is_solved() + + setup_stock(synthesis_route, {"Cl", "CO"}) + assert not synthesis_route.is_solved() + + +def test_extract_monograms(synthesis_route): + monograms = synthesis_route.reaction_ngrams(1, "reaction_smiles") + + assert len(monograms) == 2 + assert monograms[0] == ("CO.Clc1ccccc1>>COc1ccccc1",) + assert monograms[1] == ("Cl.c1ccccc1>>Clc1ccccc1",) + + +def test_extract_bigrams(synthesis_route): + bigrams = synthesis_route.reaction_ngrams(2, "reaction_smiles") + + assert len(bigrams) == 1 + assert bigrams[0] == ("CO.Clc1ccccc1>>COc1ccccc1", "Cl.c1ccccc1>>Clc1ccccc1") + + +def test_extract_bigrams_branched(branched_synthesis_route): + bigrams = branched_synthesis_route.reaction_ngrams(2, "reaction_smiles") + + assert len(bigrams) == 2 + assert bigrams[0] == ("CO.Clc1ccccc1>>COc1ccccc1", "C.O>>CO") + assert bigrams[1] == ("CO.Clc1ccccc1>>COc1ccccc1", "Cl.c1ccccc1>>Clc1ccccc1") + + +def test_extract_trigrams_too_short(synthesis_route): + trigrams = synthesis_route.reaction_ngrams(3, "reaction_smiles") + + assert len(trigrams) == 0 + + +def test_extract_grams_augmentable(augmentable_sythesis_route): + route = augmentable_sythesis_route + bigrams = route.reaction_ngrams(2, "hash") + + assert bigrams == [("xyz", "xyy"), ("xyy", "xxz")] + + trigrams = route.reaction_ngrams(3, "hash") + assert trigrams == [("xyz", "xyy", "xxz")] diff --git a/tests/test_routes_comparison.py b/tests/test_routes_comparison.py new file mode 100644 index 0000000..7c9097e --- /dev/null +++ b/tests/test_routes_comparison.py @@ -0,0 +1,85 @@ +import json +from socket import timeout + +import pytest + +from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.comparison import ( + simple_route_similarity, + simple_bond_forming_similarity, + atom_matching_bonanza_similarity, + route_distances_calculator, +) + + +@pytest.fixture +def example_routes(shared_datadir): + with open(shared_datadir / "atorvastatin_routes.json", "r") as fileobj: + dicts = json.load(fileobj) + return [SynthesisRoute(dicts[0]), SynthesisRoute(dicts[1])] + + +def test_bond_similarity(example_routes): + similarity = simple_bond_forming_similarity(example_routes) + + assert similarity.tolist() == [[1, 0.5], [0.5, 1.0]] + + +def test_atom_similarity(example_routes): + similarity = atom_matching_bonanza_similarity(example_routes) + + assert pytest.approx(similarity.tolist()[0], rel=1e-4) == [1, 0.6871] + assert pytest.approx(similarity.tolist()[1], rel=1e-4) == [0.6871, 1] + + +def test_simple_route_similarity(example_routes): + similarity = simple_route_similarity(example_routes) + + assert pytest.approx(similarity.tolist()[0], rel=1e-4) == [1, 0.5861] + assert pytest.approx(similarity.tolist()[1], rel=1e-4) == [0.5861, 1] + + +def test_simple_route_similarity_one_no_rxns(example_routes): + example_routes[0].max_depth = 0.0 + example_routes[0].reaction_tree["children"] = [] + + similarity = simple_route_similarity(example_routes) + + assert pytest.approx(similarity.tolist()[0], rel=1e-4) == [1, 0.0] + assert pytest.approx(similarity.tolist()[1], rel=1e-4) == [0.0, 1] + + +def test_simple_route_similarity_two_no_rxns(example_routes): + for route in example_routes: + route.max_depth = 0.0 + route.reaction_tree["children"] = [] + + similarity = simple_route_similarity(example_routes) + + assert pytest.approx(similarity.tolist()[0], rel=1e-4) == [1, 1.0] + assert pytest.approx(similarity.tolist()[1], rel=1e-4) == [1.0, 1] + + +def test_ted_distance_calculator(example_routes): + ted_distance_calc = route_distances_calculator(model="ted") + + route_distances = ted_distance_calc(example_routes) + + assert route_distances.shape == (2, 2) + assert route_distances[0, 1] == route_distances[1, 0] + assert route_distances[0, 0] == route_distances[1, 1] == 0 + + +def test_ted_distance_calculator_with_kwargs(example_routes): + ted_distance_calc = route_distances_calculator(model="ted", timeout=10) + + route_distances = ted_distance_calc(example_routes) + + assert route_distances.shape == (2, 2) + assert route_distances[0, 1] == route_distances[1, 0] + assert route_distances[0, 0] == route_distances[1, 1] == 0 + + +def test_lstm_distance_calulator_not_implemented(example_routes): + with pytest.raises(NotImplementedError): + _ = route_distances_calculator(model="lstm") diff --git a/tests/test_routes_readers.py b/tests/test_routes_readers.py index b6cb5fa..8d1755d 100644 --- a/tests/test_routes_readers.py +++ b/tests/test_routes_readers.py @@ -6,6 +6,7 @@ from rxnutils.routes.readers import ( read_reactions_dataframe, read_aizynthcli_dataframe, + read_rdf_file, ) @@ -68,3 +69,14 @@ def test_create_route_from_aizynth_cli(shared_datadir): routes[0][0].reaction_smiles()[-1] == "NC1CCCCC1.C1=CCC=C1>>NC1CCCC(C2C=CC=C2)C1" ) + + +def test_create_route_from_rdf(shared_datadir): + filename = str(shared_datadir / "example_route.rdf") + + route = read_rdf_file(filename) + + reactions = route.reaction_smiles() + assert len(reactions) == 2 + assert reactions[0] == "CCC=O>>CCC(=O)O" + assert reactions[1] == "CCCN>>CCC=O" diff --git a/tests/test_routes_scoring.py b/tests/test_routes_scoring.py new file mode 100644 index 0000000..511e71a --- /dev/null +++ b/tests/test_routes_scoring.py @@ -0,0 +1,99 @@ +import copy +import json +import os + +import pandas as pd +import pytest +import requests + +from rxnutils.routes.scoring import ( + route_sorter, + route_ranks, + badowski_route_score, + retro_bleu_score, + ngram_overlap_score, +) +from rxnutils.routes.retro_bleu.ngram_collection import NgramCollection + + +def test_route_sorter(synthesis_route, setup_stock): + route1 = copy.deepcopy(synthesis_route) + setup_stock(route1, {"Cl", "CO"}) + route2 = copy.deepcopy(synthesis_route) + setup_stock(route2, {"Cl", "CO", "c1ccccc1"}) + routes = [route1, route2] + + sorted_routes, route_scores = route_sorter(routes, badowski_route_score) + + assert route_scores == pytest.approx([6.625, 20.6875], abs=1e-4) + assert sorted_routes[0] is route2 + assert sorted_routes[1] is route1 + + +def test_route_rank(): + + assert route_ranks([4.0, 5.0, 5.0]) == [1, 2, 2] + assert route_ranks([4.0, 4.0, 5.0]) == [1, 1, 2] + assert route_ranks([4.0, 5.0, 6.0]) == [1, 2, 3] + assert route_ranks([4.0, 5.0, 5.0, 6.0]) == [1, 2, 2, 3] + + +def test_badowski_score(synthesis_route, setup_stock): + setup_stock(synthesis_route, {"Cl", "CO"}) + + assert badowski_route_score(synthesis_route) == pytest.approx(20.6875, abs=1e-4) + + setup_stock(synthesis_route, {"Cl", "CO", "c1ccccc1"}) + + assert badowski_route_score(synthesis_route) == pytest.approx(6.625, abs=1e-4) + + +def test_read_and_write_ngram_collection(tmpdir): + collection = NgramCollection(2, "dummy", {("a", "b"), ("a", "c")}) + + filename = str(tmpdir / "ngrams.json") + collection.save_to_file(filename) + + assert os.path.exists(filename) + + collection2 = NgramCollection.from_file(filename) + + assert collection2.nitems == collection.nitems + assert collection2.metadata_key == collection.metadata_key + assert collection2.ngrams == collection.ngrams + + +def test_create_ngram_collection(augmentable_sythesis_route, tmpdir): + filename = str(tmpdir / "routes.json") + with open(filename, "w") as fileobj: + json.dump([augmentable_sythesis_route.reaction_tree], fileobj) + + collection = NgramCollection.from_tree_collection(filename, 2, "hash") + + assert collection.nitems == 2 + assert collection.metadata_key == "hash" + assert collection.ngrams == {("xyz", "xyy"), ("xyy", "xxz")} + + +def test_ngram_overlap_score(augmentable_sythesis_route): + ref = NgramCollection(2, "hash", {("xyz", "xyy"), ("xxyy", "xxz")}) + + score = ngram_overlap_score(augmentable_sythesis_route, ref) + + assert score == 0.5 + + +def test_retro_bleu_score(augmentable_sythesis_route): + ref = NgramCollection(2, "hash", {("xyz", "xyy"), ("xxyy", "xxz")}) + + score = retro_bleu_score(augmentable_sythesis_route, ref) + + assert pytest.approx(score, abs=1e-4) == 4.3670 + + +def test_retro_bleu_score_short_nsteps(augmentable_sythesis_route): + ref = NgramCollection(2, "hash", {("xyz", "xyy"), ("xxyy", "xxz")}) + + score = retro_bleu_score(augmentable_sythesis_route, ref, 1) + + assert pytest.approx(score, abs=1e-4) == 3.0443 diff --git a/tests/test_ted.py b/tests/test_ted.py new file mode 100644 index 0000000..207b2a4 --- /dev/null +++ b/tests/test_ted.py @@ -0,0 +1,367 @@ +import json + +import pytest + +from rxnutils.routes.base import SynthesisRoute +from rxnutils.routes.ted.utils import ( + AptedConfig, + TreeContent, + StandardFingerprintFactory, +) +from rxnutils.routes.ted.reactiontree import ReactionTreeWrapper +from rxnutils.routes.ted.distances_calculator import ted_distances_calculator + + +def collect_smiles(tree, query_type, smiles_list): + if tree["type"] == query_type: + smiles_list.append(tree["smiles"]) + for child in tree.get("children", []): + collect_smiles(child, query_type, smiles_list) + + +node1 = {"type": "mol", "fingerprint": [0, 1, 0], "children": ["A", "B", "C"]} + +node2 = {"type": "mol", "fingerprint": [1, 1, 0]} + +example_tree = { + "type": "mol", + "smiles": "Cc1ccc2nc3ccccc3c(Nc3ccc(NC(=S)Nc4ccccc4)cc3)c2c1", + "children": [ + { + "type": "reaction", + "metadata": {}, + "children": [ + { + "type": "mol", + "smiles": "Cc1ccc2nc3ccccc3c(Cl)c2c1", + }, + { + "type": "mol", + "smiles": "Nc1ccc(NC(=S)Nc2ccccc2)cc1", + }, + ], + } + ], +} + + +def test_rename_cost_different_types(): + config = AptedConfig() + + cost = config.rename({"type": "type1"}, {"type": "type2"}) + + assert cost == 1 + + +def test_rename_cost_same_types(): + config = AptedConfig() + + cost = config.rename(node1, node2) + + assert cost == 0.5 + + +def test_get_children_fixed(): + config = AptedConfig() + + assert config.children(node1) == ["A", "B", "C"] + + +def test_get_children_random(): + config = AptedConfig(randomize=True) + + children = config.children(node1) + + assert len(children) == 3 + for expected_child in ["A", "B", "C"]: + assert expected_child in children + + +@pytest.mark.parametrize( + "route_index", + [1, 2], +) +def test_create_wrapper(load_reaction_tree, route_index): + tree = load_reaction_tree("example_routes.json", route_index) + route = SynthesisRoute(tree) + wrapper = ReactionTreeWrapper(route) + + assert wrapper.info["content"] == TreeContent.MOLECULES + assert wrapper.info["tree count"] == 4 + assert wrapper.first_tree["type"] == "mol" + assert len(wrapper.trees) == 4 + + wrapper = ReactionTreeWrapper(route, TreeContent.REACTIONS) + + assert wrapper.info["content"] == TreeContent.REACTIONS + assert wrapper.info["tree count"] == 1 + assert wrapper.first_tree["type"] == "reaction" + assert len(wrapper.trees) == 1 + + wrapper = ReactionTreeWrapper(route, TreeContent.BOTH) + + assert wrapper.info["content"] == TreeContent.BOTH + assert wrapper.info["tree count"] == 4 + assert len(wrapper.trees) == 4 + + +def test_create_wrapper_no_reaction(): + tree = {"smiles": "CCC", "type": "mol"} + route = SynthesisRoute(tree) + + wrapper = ReactionTreeWrapper(route) + assert wrapper.info["tree count"] == 1 + assert len(wrapper.trees) == 1 + + with pytest.raises(ValueError): + ReactionTreeWrapper(route, TreeContent.REACTIONS) + + wrapper = ReactionTreeWrapper(route, TreeContent.BOTH) + assert wrapper.info["tree count"] == 1 + assert wrapper.first_tree["type"] == "mol" + assert len(wrapper.trees) == 1 + + +def test_create_one_tree_of_molecules(load_reaction_tree): + tree = load_reaction_tree("example_routes.json", 0) + route = SynthesisRoute(tree) + + wrapper = ReactionTreeWrapper(route, exhaustive_limit=1) + + assert wrapper.info["tree count"] == 2 + assert len(wrapper.trees) == 1 + + assert wrapper.first_tree["smiles"] == tree["smiles"] + assert len(wrapper.first_tree["children"]) == 2 + + child_smiles = [child["smiles"] for child in wrapper.first_tree["children"]] + expected_smiles = [node["smiles"] for node in tree["children"][0]["children"]] + assert child_smiles == expected_smiles + + +def test_create_one_tree_of_reactions(load_reaction_tree): + tree = load_reaction_tree("example_routes.json", 0) + route = SynthesisRoute(tree) + + wrapper = ReactionTreeWrapper( + route, content=TreeContent.REACTIONS, exhaustive_limit=1 + ) + + assert wrapper.info["tree count"] == 1 + assert len(wrapper.trees) == 1 + + rxn_nodes = [] + collect_smiles(tree, "reaction", rxn_nodes) + assert wrapper.first_tree["smiles"] == rxn_nodes[0] + assert len(wrapper.first_tree["children"]) == 0 + + +def test_create_one_tree_of_everything(load_reaction_tree): + tree = load_reaction_tree("example_routes.json", 0) + route = SynthesisRoute(tree) + + wrapper = ReactionTreeWrapper(route, content=TreeContent.BOTH, exhaustive_limit=1) + + assert wrapper.info["tree count"] == 2 + assert len(wrapper.trees) == 1 + + mol_nodes = [] + collect_smiles(tree, "mol", mol_nodes) + rxn_nodes = [] + collect_smiles(tree, "reaction", rxn_nodes) + assert wrapper.first_tree["smiles"] == tree["smiles"] + assert len(wrapper.first_tree["children"]) == 1 + + child1 = wrapper.first_tree["children"][0] + assert child1["smiles"] == rxn_nodes[0] + assert len(child1["children"]) == 2 + + child_smiles = [child["smiles"] for child in child1["children"]] + assert child_smiles == mol_nodes[1:] + + +def test_create_all_trees_of_molecules(load_reaction_tree): + tree = load_reaction_tree("example_routes.json", 0) + route = SynthesisRoute(tree) + + wrapper = ReactionTreeWrapper(route) + + assert wrapper.info["tree count"] == 2 + assert len(wrapper.trees) == 2 + + mol_nodes = [] + collect_smiles(tree, "mol", mol_nodes) + # Assert first tree + assert wrapper.first_tree["smiles"] == mol_nodes[0] + assert len(wrapper.first_tree["children"]) == 2 + + child_smiles = [child["smiles"] for child in wrapper.first_tree["children"]] + assert child_smiles == mol_nodes[1:] + + # Assert second tree + assert wrapper.trees[1]["smiles"] == mol_nodes[0] + assert len(wrapper.trees[1]["children"]) == 2 + + child_smiles = [child["smiles"] for child in wrapper.trees[1]["children"]] + assert child_smiles == mol_nodes[1:][::-1] + + +def test_create_two_trees_of_everything(load_reaction_tree): + tree = load_reaction_tree("example_routes.json", 0) + route = SynthesisRoute(tree) + + wrapper = ReactionTreeWrapper(route, content=TreeContent.BOTH) + + assert wrapper.info["tree count"] == 2 + assert len(wrapper.trees) == 2 + + mol_nodes = [] + collect_smiles(tree, "mol", mol_nodes) + rxn_nodes = [] + collect_smiles(tree, "reaction", rxn_nodes) + # Assert first tree + assert wrapper.first_tree["smiles"] == mol_nodes[0] + assert len(wrapper.first_tree["children"]) == 1 + + child1 = wrapper.first_tree["children"][0] + assert child1["smiles"] == rxn_nodes[0] + assert len(child1["children"]) == 2 + + child_smiles = [child["smiles"] for child in child1["children"]] + assert child_smiles == mol_nodes[1:] + + # Assert second tree + assert wrapper.trees[1]["smiles"] == mol_nodes[0] + assert len(wrapper.trees[1]["children"]) == 1 + + child1 = wrapper.trees[1]["children"][0] + assert child1["smiles"] == rxn_nodes[0] + assert len(child1["children"]) == 2 + + child_smiles = [child["smiles"] for child in child1["children"]] + assert child_smiles == mol_nodes[1:][::-1] + + +def test_route_self_distance(load_reaction_tree): + tree = load_reaction_tree("example_routes.json", 0) + route = SynthesisRoute(tree) + + wrapper = ReactionTreeWrapper(route, exhaustive_limit=1) + + assert wrapper.distance_to(wrapper) == 0.0 + + +def test_route_distances_random(load_reaction_tree): + tree1 = load_reaction_tree("example_routes.json", 0) + route1 = SynthesisRoute(tree1) + wrapper1 = ReactionTreeWrapper(route1, exhaustive_limit=1) + + tree2 = load_reaction_tree("example_routes.json", 1) + route2 = SynthesisRoute(tree2) + wrapper2 = ReactionTreeWrapper(route2, exhaustive_limit=1) + + distances = list(wrapper1.distance_iter(wrapper2, exhaustive_limit=1)) + + assert len(distances) == 2 + assert pytest.approx(distances[0], abs=1e-2) == 2.6522 + + +def test_route_distances_exhaustive(load_reaction_tree): + tree1 = load_reaction_tree("example_routes.json", 0) + route1 = SynthesisRoute(tree1) + wrapper1 = ReactionTreeWrapper(route1, exhaustive_limit=2) + + tree2 = load_reaction_tree("example_routes.json", 1) + route2 = SynthesisRoute(tree2) + wrapper2 = ReactionTreeWrapper(route2, exhaustive_limit=2) + + distances = list(wrapper1.distance_iter(wrapper2, exhaustive_limit=40)) + + assert len(distances) == 2 + assert pytest.approx(distances[0], abs=1e-2) == 2.6522 + assert pytest.approx(min(distances), abs=1e-2) == 2.6522 + + +def test_route_distances_semi_exhaustive(load_reaction_tree): + tree1 = load_reaction_tree("example_routes.json", 0) + route1 = SynthesisRoute(tree1) + wrapper1 = ReactionTreeWrapper(route1, exhaustive_limit=1) + + tree2 = load_reaction_tree("example_routes.json", 1) + route2 = SynthesisRoute(tree2) + wrapper2 = ReactionTreeWrapper(route2, exhaustive_limit=2) + + distances = list(wrapper1.distance_iter(wrapper2, exhaustive_limit=1)) + + assert len(distances) == 2 + assert pytest.approx(distances[0], abs=1e-2) == 2.6522 + assert pytest.approx(min(distances), abs=1e-2) == 2.6522 + + +def test_route_distances_longer_routes(load_reaction_tree): + tree1 = load_reaction_tree("longer_routes.json", 0) + route1 = SynthesisRoute(tree1) + wrapper1 = ReactionTreeWrapper(route1, content="both") + + tree2 = load_reaction_tree("longer_routes.json", 1) + route2 = SynthesisRoute(tree2) + wrapper2 = ReactionTreeWrapper(route2, content="both") + + distances = list(wrapper1.distance_iter(wrapper2)) + + assert len(distances) == 21 + assert pytest.approx(distances[0], abs=1e-2) == 4.14 + + +def test_distance_matrix(load_reaction_tree): + routes = [ + SynthesisRoute(load_reaction_tree("example_routes.json", idx)) + for idx in range(3) + ] + + dist_mat = ted_distances_calculator(routes, content="molecules") + + assert len(dist_mat) == 3 + assert pytest.approx(dist_mat[0, 1], abs=1e-2) == 2.6522 + assert pytest.approx(dist_mat[0, 2], abs=1e-2) == 3.0779 + assert pytest.approx(dist_mat[2, 1], abs=1e-2) == 0.7483 + + +def test_distance_matrix_timeout(load_reaction_tree): + routes = [ + SynthesisRoute(load_reaction_tree("example_routes.json", idx)) + for idx in range(3) + ] + + with pytest.raises(ValueError): + ted_distances_calculator(routes, content="molecules", timeout=0) + + +def test_fingerprint_calculations(): + example_route = SynthesisRoute(example_tree) + wrapper = ReactionTreeWrapper( + example_route, content="both", fp_factory=StandardFingerprintFactory(nbits=128) + ) + + fp = wrapper.first_tree["sort_key"] + mol1 = "1000010000000000000010001000100101000101100000010000010000100001" + mol2 = "1100000001110110011000100010000001001000000100100000110000100100" + assert fp == mol1 + mol2 + + fp = wrapper.first_tree["children"][0]["sort_key"] + rxn1 = "00000-1000000-1000-100-2000000000000000000000000000-10-20000000000000-1" + rxn2 = "-10000000001000100-10000-100-10000000000-10-1000000000000-11000-10000100" + assert fp == rxn1 + rxn2 + + +def test_custom_fingerprint_calculations(): + def factory(tree, parent): + if tree["type"] != "reaction": + return + tree["fingerprint"] = [1, 2, 3, 4] + + example_route = SynthesisRoute(example_tree) + wrapper = ReactionTreeWrapper(example_route, content="both", fp_factory=factory) + + assert wrapper.first_tree["sort_key"] == "" + assert wrapper.first_tree["children"][0]["sort_key"] == "1234" diff --git a/tests/test_uspto.py b/tests/test_uspto.py index 49d9c2e..623783f 100644 --- a/tests/test_uspto.py +++ b/tests/test_uspto.py @@ -1,9 +1,10 @@ import pandas as pd from rxnutils.data.uspto.combine import main as combine_uspto +from rxnutils.data.uspto.uspto_yield import UsptoYieldCuration -def test_combine_uspto(shared_datadir, tmpdir): +def test_combine_uspto(shared_datadir): combine_uspto( ["--filenames", "uspto_example_reactions.rsmi", "--folder", str(shared_datadir)] ) @@ -13,3 +14,45 @@ def test_combine_uspto(shared_datadir, tmpdir): assert len(data) == 9 assert data.columns.to_list() == ["ID", "Year", "ReactionSmiles"] assert data["ID"].to_list()[:2] == ["US03930836;;0", "US03930836;;1"] + + +def test_uspto_yield_curation(): + + data = pd.DataFrame( + { + "TextMinedYield": [ + "~10", + "50%", + ">40", + ">=50", + "<30", + "20 to 50", + "-50", + "100", + "50", + "", + "", + ], + "CalculatedYield": [ + "12%", + "110", + "40", + "51", + "", + "40", + "-40", + "120", + "40", + "70", + "", + ], + } + ) + action = UsptoYieldCuration() + + data2 = action(data) + + expected = [False] * 6 + [True] + [False] * 3 + [True] + assert data2["CuratedYield"].isna().tolist() == expected + assert data2["CuratedYield"].tolist()[:6] == [12, 50, 40, 51, 30, 50] + assert data2["CuratedYield"].tolist()[7:-1] == [100, 50, 70] diff --git a/tests/test_validation.py b/tests/test_validation.py new file mode 100644 index 0000000..882522f --- /dev/null +++ b/tests/test_validation.py @@ -0,0 +1,21 @@ +import pytest + +from rxnutils.routes.utils.validation import validate_dict + + +@pytest.mark.parametrize( + "route_index", + [0, 1, 2], +) +def test_validate_example_trees(load_reaction_tree, route_index): + validate_dict(load_reaction_tree("example_routes.json", route_index)) + + +def test_validate_only_mols(): + dict_ = { + "smiles": "CCC", + "type": "mol", + "children": [{"smiles": "CCC", "type": "mol"}], + } + with pytest.raises(ValueError, match="String should match pattern"): + validate_dict(dict_)