From 41537856f5415b41b2614dcb199d42ecde95e043 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:01:49 +0000 Subject: [PATCH 01/36] chore(deps): update all non-major dependencies (#876) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- uv.lock | 96 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/uv.lock b/uv.lock index a4d20ff52..e49e17535 100644 --- a/uv.lock +++ b/uv.lock @@ -1226,52 +1226,52 @@ wheels = [ [[package]] name = "hypothesis" -version = "6.165.10" +version = "6.167.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "sortedcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/e2/0fad246d2b6330e1f78479bfc566b5c22be82aee8a865cde9a08f648487d/hypothesis-6.165.10.tar.gz", hash = "sha256:68b45e09834cd80523cb1eb274463073c7a9af4e4ef7cff34d9615f355572d32", size = 503703, upload-time = "2026-08-16T22:56:15.404Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/52/6f0a9b7aab24b0635e2238f3fbddea5b54b17879ac813df42a3cc3384c5c/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76a7be86d986223b9f1bdb7e7cbcdb048649901fdb956c598ef73bdab1786cd5", size = 1108009, upload-time = "2026-08-16T22:54:53.082Z" }, - { url = "https://files.pythonhosted.org/packages/f6/06/8d0d4e11ff02350d09ec9f9e90af354158e59e16a8907ba5199a4ff2d7e8/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:717aea574e0e5edba2868aa66b1caae335d8f1ad3fb29f01dd6502953fa823a1", size = 1136596, upload-time = "2026-08-16T22:54:54.443Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/01a1e440f2e38dc1ccf5d597af5b8a0bee5f21b674c99c123b5554de9690/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4334058033e0214475f019e15492a50f3854fe8728cf51fe25c6191a2c3f8e52", size = 1135234, upload-time = "2026-08-16T22:55:08.911Z" }, - { url = "https://files.pythonhosted.org/packages/7d/18/8a26c24d3d9db20265f39df341ab265858c094e209571e3179cf237935f4/hypothesis-6.165.10-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2abb50cf1cf77d721de0a24c3f99d9c4ffdeb2cbd1e12aebb5a7a93e2b6b6d1f", size = 1157528, upload-time = "2026-08-16T22:56:02.159Z" }, - { url = "https://files.pythonhosted.org/packages/ea/8e/ce3c829b1937402d7944420ca26a05a0c8563e894dcff03d34ffa279d306/hypothesis-6.165.10-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:3de69aa8b924b400291a3cc42aaf78e6ab65c905a3e7e1a5dc39d95ef1b428cb", size = 1112870, upload-time = "2026-08-16T22:54:55.919Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1b/4c4926d6c9a2b5d7cc090cc1e91219d6796102aa2a2c4b8f961c939e60b5/hypothesis-6.165.10-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5841331c504e02d7c334591681cb8587cdd59dee7e149db6d3db8e3f9e9f02eb", size = 1149683, upload-time = "2026-08-16T22:55:30.567Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f9/df24eb28412f82465e2b7707f0ff1ec274d580bce389d4d9156617dc7bba/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2d0e0f8263d34dd8fa3b39eaa9a50bba56a8470b3dd9ebf6672d10840abe063e", size = 1283402, upload-time = "2026-08-16T22:54:18.054Z" }, - { url = "https://files.pythonhosted.org/packages/4d/07/c2b2a761300cf60b90ccebba4328175331e67d34f4fbd39429a7ddcdce49/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0c4e6869817c3cfdf5a2b4d348497b95159bdecb3365be732c9b8570e36a4eef", size = 1409948, upload-time = "2026-08-16T22:54:22.343Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ec/1c2bf1acdd0e273d81f833f85caf0ae5423db68a783554992fca36e6c541/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9f07ae36c3b093e13687a894e79fe69e98a94c0b67fef656c575247682218143", size = 1265023, upload-time = "2026-08-16T22:54:41.402Z" }, - { url = "https://files.pythonhosted.org/packages/3d/a8/7f984908b7391160c7801b84e51ca8e4ba88c89e8d8811aa1aa7c03de73c/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:aff1f584c9538e8979cd180b1d70bf99bc16be19d4666414f49e5942b21a4f2c", size = 1282698, upload-time = "2026-08-16T22:56:06.998Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/3a5d91c2d0250521736c42dfa2402b75049bc5fe2fb716c10bc84bb91ed1/hypothesis-6.165.10-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1f2c4db25fb8ec1a16a8dba580666337b8ffb1887c4cf1750cc954313897cef7", size = 1324816, upload-time = "2026-08-16T22:54:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/04/66/4c71c5be7a49d84b8c3a9278c1807c4c81181ab5474beb27df9d4c40dc0e/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9ff356e97e3ab09db07c8b675efa67340103874a0bae7465acb83dad7a35f7f", size = 1106830, upload-time = "2026-08-16T22:55:10.389Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c4/e2cbd2810e79f7a452a8ea9f6c6438ee718ce938d8cc12252cf0b36a81d3/hypothesis-6.165.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a380bc99aa3b035e6a95a2201bf792d4082a04ca75babcc21849c2d0914bb28", size = 1156952, upload-time = "2026-08-16T22:55:53.35Z" }, - { url = "https://files.pythonhosted.org/packages/a8/8b/794ced36864825492ac3712d5acab5a257b4601e6a9dc2ccdd3937198f87/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9acb2c4d9cb532c3fedea74159f7b923c8c036328c9239b4049e7aa073bdd81", size = 1280780, upload-time = "2026-08-16T22:54:34.983Z" }, - { url = "https://files.pythonhosted.org/packages/5d/2d/550525442cdbcc2daf1f9bdd8ba35bcbde63db7c7a22f2ef137fbb49df2f/hypothesis-6.165.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8660572b2d424bf5369ea8990985225f70bd1615b76ecd9c25588a3b9307009f", size = 1324130, upload-time = "2026-08-16T22:55:48.659Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/922e8b3f9a706dd89d440b9545d2c6231c65e74da1c1fee3ff36c251b9c4/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed68e27b8a61e57a3ccdc7c5a14499e00b54dfe223087204d5d40b3b5ef58b6d", size = 1106763, upload-time = "2026-08-16T22:55:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/01/39/f5b9a5d390d4edd1ad472334493ac442963ebeb4daaa74ff4bdac6ef292f/hypothesis-6.165.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6caadcd1afb62630ff5c5ff353626eaa616553a5971295ad6dc2b19ca8a39620", size = 1156778, upload-time = "2026-08-16T22:54:33.824Z" }, - { url = "https://files.pythonhosted.org/packages/b5/5f/5fbe1be4326337fd6acefe2d18ed44007ee1dc1f98fe5b3c0eb22942364d/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9145fe43ebb22e66672967c3fab411793b226ed776e4fe282271bca6ad3c0bb", size = 1280756, upload-time = "2026-08-16T22:55:54.834Z" }, - { url = "https://files.pythonhosted.org/packages/25/c0/cf6f9e1ef632a1a75694eed0db3a02e6fc75c367a363e94acee52f043c64/hypothesis-6.165.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79900a9920a0b1d3a626c03a90ac6bf7042e78d46906a565b86a0dbe926f1d96", size = 1323889, upload-time = "2026-08-16T22:55:56.567Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/58d4f842895220b793c53fc94a6489705b3665bb4d0ae4d338ce03fdf9fb/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f76d1562643693b8a40066f1f96af795b93fd9bcfc9690a1af2ff4c5867ee29e", size = 1107271, upload-time = "2026-08-16T22:54:50.266Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b8/206468912d2153306bb8a41afdfc59e45b7a73a0495bbe4b9cb4f0e79c1d/hypothesis-6.165.10-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60cab3ab4ea468d31a33739ffd7e94ec3e37dea891d65a6582ecc8a477175191", size = 1156915, upload-time = "2026-08-16T22:54:25.89Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d3/bf5a22929b70a4cfd3edf69c5642b029b27ddb5cfda48fa295d384b01abb/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22cf19388f0ff6ced8eb3e49c903d14938e4ed909d93bf28383eef451511e424", size = 1281205, upload-time = "2026-08-16T22:54:44.083Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/d7b2ba444d36fc84d4779f4431e74dd9b023dc63bcf282199f6e48ad39f4/hypothesis-6.165.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:057d0232f1224dcd0b7698902551a4341a7399f90670b036db6c4376715fe889", size = 1324243, upload-time = "2026-08-16T22:55:41.123Z" }, - { url = "https://files.pythonhosted.org/packages/d1/95/afe6b531fd01928c6f63d394ee413fa2338d088b2b44efcc23596b54477e/hypothesis-6.165.10-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ab0f2e9d7d7d4db257f7cf53de3706c2baf124269571f20ffc2bcd6781f03063", size = 616382, upload-time = "2026-08-16T22:55:18.449Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0b/f47506241f9d5a5a2efe4c65b6bf4830e9d9576e5d3779007a260699e608/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cf3b612542ba174c9da4000b59a4f4c81e8d66f87509be85d3a1b71b5c36413", size = 1105499, upload-time = "2026-08-16T22:54:51.864Z" }, - { url = "https://files.pythonhosted.org/packages/84/fe/abb3909b7089835112fbe75bf00d817d733b3a8032759783db0a24ff1e56/hypothesis-6.165.10-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69ec5be85ef508e206153bed8eafd03f7995dc464356c8bbb279a1e2b7d56f3", size = 1155685, upload-time = "2026-08-16T22:54:30.94Z" }, - { url = "https://files.pythonhosted.org/packages/73/2f/1964738921640184067121ae77414522fc3f0463fc26c6e25a4f3b8e42ca/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dd207497bb985918409a1bb5db85d1875f74e1269487332113b73d1ee7c77647", size = 1279177, upload-time = "2026-08-16T22:54:40.179Z" }, - { url = "https://files.pythonhosted.org/packages/34/c5/312af8ae038d3af9cf3f7f1021c1abfe31c0d9035e4cf63519e0a7dc983e/hypothesis-6.165.10-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:00de0abdcf8c05c9d0eab735a3c49a276376b55151e6fcb903c2b39a90e5e5c3", size = 1322921, upload-time = "2026-08-16T22:54:42.7Z" }, - { url = "https://files.pythonhosted.org/packages/10/39/ef26fa79c1738dfe9cdb1a3584fb6717d26429ca6c9d011cc4fdf08130c2/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7730d8197086f65d8969a991d6728a1d420a51b19fea06535c896cb43a1e05d0", size = 1104876, upload-time = "2026-08-16T22:54:58.937Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f4/3fcc84e7637f42bf00d987093b9418083ac8db81b87392608a60f4b7c5fd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7a7980a898a3e6ebe4de1896a0507e3d519edb53fb9b4bda478c9fbeb6514558", size = 1133353, upload-time = "2026-08-16T22:54:28.635Z" }, - { url = "https://files.pythonhosted.org/packages/35/59/21c5c14179c38f8d0de3560e7f1825c083311b3013b63f817d7dc78dfcbd/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5820d009aedb7ae9cfd32f98b1ab0c0bbd6268379c4fab042218b6b655c63f8", size = 1132300, upload-time = "2026-08-16T22:56:08.539Z" }, - { url = "https://files.pythonhosted.org/packages/14/af/fbb56059961e416b2de7b9dc5352db2e8572bd5ea46892957e4c1e5548ab/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37a7ac3d34220800e1107871cc391bca1b00439875925d7d821878b8b791f245", size = 1155175, upload-time = "2026-08-16T22:55:19.824Z" }, - { url = "https://files.pythonhosted.org/packages/0f/53/77fb0c2dad445858555429c4e06cf94a59ae8d2407dd6426b5af97c84828/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:dafa7c9dbe3d802f9bcdf261b29c8a70700fb22839947f06e471f62c46b6257f", size = 1109881, upload-time = "2026-08-16T22:55:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7b/d187f673ff30e6ada640953636f978ffe64a6332f756b64163c2277f8d0c/hypothesis-6.165.10-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:90915635b9648071129b0f72c0673cf8eac9eb84cfd445c5bedef30c714b1ec2", size = 1144963, upload-time = "2026-08-16T22:56:13.428Z" }, - { url = "https://files.pythonhosted.org/packages/e0/60/31d504e364134d60af23e5f6365db0da3cf4a51b3ed3d4836e5a2cff12cf/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e1bbeb7c506b07ee0422cf9b2f7212fefa4240957f03526d38d27bc6743a0a48", size = 1278684, upload-time = "2026-08-16T22:55:22.971Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e6/89d26834a08c02f8da149e541dd40d7a96f68d9722f43146e69a77436ed7/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:2b36aaffc88625a44f91074c5bbedfdefb9b376c38d1b3c342edcd2e4c8ed16c", size = 1407202, upload-time = "2026-08-16T22:55:14.949Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/20d1e72246867ea195440092e8bb422c7ddc2f271b87b5b65679d5532719/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:18a3ea838ddea183388f8788750afa8494d79abb5358823be9782585f34445d3", size = 1261395, upload-time = "2026-08-16T22:56:05.448Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9b/ebab6c3c2b90a16abb4119198178652d12aff83cc8ec2cfde5276c69fb1e/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2a2567b3a03a4a5a7c575c191cfcce321a967df3727803817e75bffbbeaecabe", size = 1279213, upload-time = "2026-08-16T22:55:35.066Z" }, - { url = "https://files.pythonhosted.org/packages/23/78/69b219b524231d36eb20c792e1f01e7cb037e02bd0af1c29f77ed9a969c0/hypothesis-6.165.10-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8001925fa3dde51cb574e4c9de4c7efe77c4e4d64bd2fd2ef61d5651f9d04f3d", size = 1322367, upload-time = "2026-08-16T22:54:21.279Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8cee74c1390b2932406faaab76980f18946f258fa5a8afca17189b3bc655/hypothesis-6.167.1.tar.gz", hash = "sha256:62eefcb4d2791423626e9901c3027a6e0c5ffda2ac0b44b3c7e797ab9d2d5a4c", size = 505849, upload-time = "2026-08-30T19:53:09.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/a3/e0de9a82c7e790a1def0801076e0ef43110f98e95ed54a3554877d0cb66d/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f8cec74c4fad7aeb0852cb34c2134b16db05d878ad3946a53337dace7016f4", size = 1117814, upload-time = "2026-08-30T19:53:04.109Z" }, + { url = "https://files.pythonhosted.org/packages/71/a4/8dd6bdc909324d1c39da1c86d65f75512ae049c159952af4cfe8feb5f8d4/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd202d02d129197a5e771f8a11c7d30559927284c23ec3a8bd4f37a7955964d1", size = 1141639, upload-time = "2026-08-30T19:51:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bd/c13ed6145c360d0770415efd7d5a7e63c29905aeef52ab88004fe7e7f924/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b1e393ab01b71f683ba2a783785871cc6b81a6e41017780c64a5bc0b99759ae", size = 1143334, upload-time = "2026-08-30T19:50:38.045Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a8/060d79ed8504b54ced9ad16f33d674b1b98a9debe9733c02709d7dd5c71c/hypothesis-6.167.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c385c7741893404306f9e5559ab3835432e85e7c153e25f854c502c410bbcbb", size = 1163345, upload-time = "2026-08-30T19:53:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f7/6e68e2b705f729a6f7b4f41030022b1a5264c5434d3bcd917233d6801c6a/hypothesis-6.167.1-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:94920ca1fae70c26b0bd3fabbeef9437ffc17a39fe85696fb9a86187d92f6dba", size = 1123029, upload-time = "2026-08-30T19:52:03.134Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c0/d274fe37ed5ecd5ad8ed555edc1f5e2abc8e1c3be3d5404b7edd5cc353a8/hypothesis-6.167.1-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8d90ded2ffdc7e56b5e571993f384b52fade0a7b424e614f999cc2491789970", size = 1154003, upload-time = "2026-08-30T19:50:55.053Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2f/2b5bb386f43fc965eb86fd69fcb2bd62c08cb6d7c6708a40dc39b3b97440/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:40cd5de7dd252942a08480639f5850594b1aca4a463e8a7f15e1fb6c2c3760c1", size = 1293729, upload-time = "2026-08-30T19:52:59.481Z" }, + { url = "https://files.pythonhosted.org/packages/ac/32/22436b072d79011fe81abb933edcd2476057c7b971588c5f3caf07519a88/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b9c33f921ddc7fea93660eca408b25fe755516e22ec7ab21cb9951031f1cd608", size = 1419248, upload-time = "2026-08-30T19:50:52.903Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3e/abf39faaff0f78112112a82316a5c9fe472574480c1ecee526734775b812/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:495989cf0a5ee03f7f9598ee9efeaabf15fd861ec52b5a9d6435849453e17e5d", size = 1274903, upload-time = "2026-08-30T19:52:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/29/83/69b89ed5692ba3aad117facfb9ce099633a22c35acc3d64829b72253ec8c/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:bc73c46ce8ff93b0eb220f2b75adcbd9fcc9112078a74d3522d2532ad8069bad", size = 1294185, upload-time = "2026-08-30T19:50:35.038Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1e/55dfcbe45c72df0a5c5b86a6b7c9365121acab69c2cc060bd55336a48c8f/hypothesis-6.167.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36e83e1d7e97aaacbf6cd778e14a841344f848a674b20dfe4fe997546a6a2151", size = 1330013, upload-time = "2026-08-30T19:52:54.841Z" }, + { url = "https://files.pythonhosted.org/packages/13/2a/60c18a493215c22c9cfcb4574b381497bd1971eed9fb5f9831b26e73cffb/hypothesis-6.167.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af3c09428e553b1dd2f9abbc4738377c58bf6d74cb0b8b528cc1dde3a9cdfbe8", size = 1116743, upload-time = "2026-08-30T19:50:49.293Z" }, + { url = "https://files.pythonhosted.org/packages/12/1f/b6796f11d6502e0b1764aec99f2792ca60382b6a45e26bbe163dc5757980/hypothesis-6.167.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04f807b85d425a7005e8a24498ca832bf5590f0d306737471d94c842569cecef", size = 1162718, upload-time = "2026-08-30T19:52:47.544Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6c/e3cf35474b799e299fa08980b6756f500d730781686916ab65f87cbc0613/hypothesis-6.167.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:803c6a98ff66cee4caf03245bfd00e442a907264031b994a3a650dc6e4786f51", size = 1292417, upload-time = "2026-08-30T19:50:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/875803c80c373a1628f8eb62f215ad23ce7b50ed61f884d6be0838ebea4a/hypothesis-6.167.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21a7122ddf072906083e3704fe3961ecbc49d7d30a9b51bcd525d977b3afe65e", size = 1329035, upload-time = "2026-08-30T19:51:46.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7d/e8039791aaca3b21557bc520a71cdb88751892f66fd1a0a459b59872e463/hypothesis-6.167.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ef7dd225f7df7d74d1c5a905592cd8b4cd348e6be639b189a43def8b0b5dd79", size = 1116749, upload-time = "2026-08-30T19:52:38.178Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b8/f9b8d93bd6178870f0daa868ca99915f6d9df1f99dc7291e9ce2743a6dc5/hypothesis-6.167.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d7585429f2263d3ceeb3474bae3871024630a7a598e71eeb4b0dcf03e291623", size = 1162599, upload-time = "2026-08-30T19:52:11.72Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/53d419094e6f8a7e7377c09de15ac23f842ab698ff07241f7b73e19bd559/hypothesis-6.167.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fb9194f450417cf35f66b6c72737cc6b8f21f20567ba4d39822c64f0d1075784", size = 1292230, upload-time = "2026-08-30T19:52:49.732Z" }, + { url = "https://files.pythonhosted.org/packages/5c/df/cf4c482323ae4f06b5326b5bdd89cf17d8232fdb3186c9913e0b19a5fa58/hypothesis-6.167.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c63a0d292a5dde3c0fe999892e76d8375a003ca40c0c00d763f3360f91be5b96", size = 1328899, upload-time = "2026-08-30T19:51:05.366Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/4c4bc3d319eac85bfe17515c9786bf49e57181ca8757886110d2cfb13d10/hypothesis-6.167.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d75d44bdead6679b6ee9a7c90d10207db865ca0c77c5212103b5ff421379f99e", size = 1116972, upload-time = "2026-08-30T19:51:52.472Z" }, + { url = "https://files.pythonhosted.org/packages/33/45/0d61ceef2739e7b96ea1faa0f3d5aa5917c8156797993bf3acbadfcd7f0a/hypothesis-6.167.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742be00d7bb53d10634e6435e5b98f51fcdbe7ed377d473ab7387d9499c87169", size = 1162776, upload-time = "2026-08-30T19:51:09.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cf/756666ce2262e90fd61fec41a95548cceab94b0669381d8f0387cd89af93/hypothesis-6.167.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b6fcdc8d03b37a902262be13112d113bb4ac87edf3b08afb47f3d1210deb038a", size = 1292748, upload-time = "2026-08-30T19:51:17.22Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b4/87eb3c695d6c37fb44f4d49f9faa2033af496e24965658942a1706e22620/hypothesis-6.167.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56e7841514276c308c2bb4d033cf01860d0fc8c76e2b79ce748a9f123eaf83b", size = 1329101, upload-time = "2026-08-30T19:51:36.313Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3b/87faa4a86533eaaa19037741fb9cdde8647f7ffdf8fd4279828ac9d81f8b/hypothesis-6.167.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:bbf4f0cad201d0b8e821e82ad828b2aec99ce6d9967779eecb2cad4d4a93debd", size = 618079, upload-time = "2026-08-30T19:52:43.226Z" }, + { url = "https://files.pythonhosted.org/packages/85/79/f1a7e088e13a641357abb9b43d75c116c2a0902711b1a25a203864b96c9b/hypothesis-6.167.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27829aa89fe2e47c5c8d13b3ce31e0f53a01a98f76a4f99cfa3369ab35362f33", size = 1115311, upload-time = "2026-08-30T19:52:40.975Z" }, + { url = "https://files.pythonhosted.org/packages/e0/38/e28b1fc20bd3d67d43cf1aab7a15daa2a24ec01d17a153e82fcf38c882f3/hypothesis-6.167.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e936777d92ae27393b4a941839bbb43c1f339b5a0e394c7f3730454cdf091b3a", size = 1161238, upload-time = "2026-08-30T19:52:20.501Z" }, + { url = "https://files.pythonhosted.org/packages/5a/de/9b4fc7992166299e0fc5c13c8766919ae57d0fb9eed5319b7a3bad4f2f17/hypothesis-6.167.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:971ce0d8a367a37c4690b83a2e7f6ef832fa3543357eb6da0da27ba078e5088a", size = 1290974, upload-time = "2026-08-30T19:51:15.673Z" }, + { url = "https://files.pythonhosted.org/packages/cc/79/ca086eea02588212ab796ee4bd7fe6ed514e10d1a99967e478691608e8d9/hypothesis-6.167.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:af84ce2416be2a65bc0ea18e64d2dbb9796b7692593b5b2064d60ea1d52ec1e2", size = 1327969, upload-time = "2026-08-30T19:52:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a4/f8ecedcf96790aab69d750afe3fcbf503229d0bb4e0c32be655385a4fc8c/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0807ae8d399162827fc1c396ab4c697a41921c2a2baacfada439771e9dc2b867", size = 1115116, upload-time = "2026-08-30T19:52:16.029Z" }, + { url = "https://files.pythonhosted.org/packages/7e/97/8bca7c262ac4fcb1ee684c04e4ba75f26541d3a30417e3743d19912d257e/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:546fef39c7aadba74bf3e592585694a71340d1775e9b3274bb3f94106dbde4b7", size = 1137812, upload-time = "2026-08-30T19:51:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/d6/07/9cb4a7fc2aa2b2ad063b446c378f0d7acfa5303e84afd1b1374ba23fd6f3/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a17a5618b6a5b84f17c8acb3bce37122647cf7a3e48b660a39d68a773bd627dd", size = 1140384, upload-time = "2026-08-30T19:52:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/1c/18/813bf7efa18f11ce938a0da22a7518a54db46d4a181ebf4cb0a8061c263f/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ff5ad833480c1e34ae902cb52fc00802b08ac6c87bd22d7e5f04fd925869608", size = 1160569, upload-time = "2026-08-30T19:51:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/52/1d/6658d9294ed33bb17da4acf06fe63b010b205ea1861f6921c38060793255/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:630eb37df80b5bc4ec6f391b13caaecc06942ff5da3aadebacf85f53bbc55757", size = 1120605, upload-time = "2026-08-30T19:53:01.848Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/a75821c0e879223a2635b8eded84ae874cb6c711b23e9930668008d0b13f/hypothesis-6.167.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc4e65f7c43b187f7a40964706b5ded1073e0c1839e9fb5e041d7ed973bb65fe", size = 1149479, upload-time = "2026-08-30T19:51:30.972Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f2/c8c3faf4ec796d6dbf36b84662806696434aef38616e5a65b46188c04262/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:e849f518cbc4e76ab15f2f1473c60dd3103da8d32399187325ceb84309105976", size = 1290423, upload-time = "2026-08-30T19:50:51.114Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e3/e0903abe7e8634cedb5414931452e82daacdd3b8b46d6348bbefcaa45f2a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:5c5a26d4d3dca0c84e01bde41df4cabaa5a373c7393f9eef372d19fe93b07ccd", size = 1415749, upload-time = "2026-08-30T19:51:48.456Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/03f09c1ecac1dfb0f4cd7fcc6dc50d9c6ea8067b295a728e242650bafe32/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4819adbc5911648f6bfaeb574f276add184b4b49f54731dbde46fd71256bb157", size = 1272086, upload-time = "2026-08-30T19:52:29.368Z" }, + { url = "https://files.pythonhosted.org/packages/7c/7c/7a63bfc0bfbaf000f71352c4faac72ff611376330a2ce2e9a1bf4668848a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:3ad7206de9c398c8da5745b69b5ba2ef45100082eeb174656490bc4f262b112c", size = 1291553, upload-time = "2026-08-30T19:52:07.545Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5c/8065bdab53bc81743ca68fc76ca53fc7531a5b3f01c0de4ba40467955d6a/hypothesis-6.167.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:96d5e8017a9508f06c8a61a6130cb0d0b4810847ed5c76923cb5cfb9952b31af", size = 1327734, upload-time = "2026-08-30T19:51:42.306Z" }, ] [[package]] @@ -1379,7 +1379,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.16.1" +version = "9.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ipython-pygments-lexers" }, @@ -1392,9 +1392,9 @@ dependencies = [ { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/96/b150fe7e25a5a29ae9ac1374e71488639605d39a1ea4abb74c9ce33af235/ipython-9.16.1.tar.gz", hash = "sha256:5a3d1f9a47ff216d6cf9cf863124f6a2c1a198d1354c546a4d24a370a283b64c", size = 4515302, upload-time = "2026-08-03T08:36:15.571Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/bc/e05ae123712ce4e1fde4408eedca1791fc1ff832684565132ea1dc646092/ipython-9.17.0.tar.gz", hash = "sha256:1dc69e6966b270fb259f676c71a21450e63607729b14a672b942914a54e8b730", size = 4538547, upload-time = "2026-08-28T09:00:58.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8e/1239df488393d61076653bfb29f759d0f60cab8e030abdf7c17c31539b51/ipython-9.16.1-py3-none-any.whl", hash = "sha256:4acae635506f6d352d94c4899a19d5f85f8bc4d230932342dca556fdab1c69b4", size = 625974, upload-time = "2026-08-03T08:36:13.654Z" }, + { url = "https://files.pythonhosted.org/packages/18/5f/f992b57e8deb8fa6c2e614b422d80698adb5107902bbc89832e203665bd1/ipython-9.17.0-py3-none-any.whl", hash = "sha256:ce647713be8fef3fab2418c515a0def4d45d6705dd102be2c6d1f3015d7368b0", size = 638698, upload-time = "2026-08-28T09:00:56.174Z" }, ] [[package]] @@ -3630,7 +3630,7 @@ wheels = [ [[package]] name = "snakemake" -version = "9.25.2" +version = "9.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "conda-inject" }, @@ -3665,9 +3665,9 @@ dependencies = [ { name = "wrapt" }, { name = "yte" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/20/ae6d3794cfc522f5d50c1b6415b5fba5b6277c56f22774f536c1dc1a02e6/snakemake-9.25.2.tar.gz", hash = "sha256:dac732795a43eeb25c2a4f3f1441c66385911703493c6059efd8bc2efac956da", size = 6808194, upload-time = "2026-08-18T11:57:37.505Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/37/f999e99357a9b30c6f36c92027ab3b6f64cade4ec54840197853297d2e04/snakemake-9.26.1.tar.gz", hash = "sha256:091b5d480c0c5eb2ef75b2568e9885091f3e2f11372de358da98ef66ef66e1e0", size = 6814489, upload-time = "2026-08-28T05:44:51.852Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/32/ea681b0de70882e7bd4368a7e15557487d869dbd6b1644bf91b2bf76beaa/snakemake-9.25.2-py3-none-any.whl", hash = "sha256:0a0d71f064eb8f99b660bb31c70372de23950449b9f3176c66970bea7fd18625", size = 1163611, upload-time = "2026-08-18T11:57:35.78Z" }, + { url = "https://files.pythonhosted.org/packages/07/16/85def09087afe9b75b789d4dd75e4ed2bbaa0c03b86b96a96bb004074b92/snakemake-9.26.1-py3-none-any.whl", hash = "sha256:076384fd68f2cf8f5816aa53210566e8fb15fbb618e2f9449845db0556cc93b5", size = 1164575, upload-time = "2026-08-28T05:44:49.693Z" }, ] [[package]] From a06df8ebb2a6147a23d2b35a3f3a1baa418513ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 02:02:35 +0000 Subject: [PATCH 02/36] chore(deps): lock file maintenance (#877) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- uv.lock | 980 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 519 insertions(+), 461 deletions(-) diff --git a/uv.lock b/uv.lock index e49e17535..19a25cf42 100644 --- a/uv.lock +++ b/uv.lock @@ -84,21 +84,33 @@ wheels = [ [[package]] name = "argon2-cffi-bindings" -version = "25.1.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, - { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, - { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, - { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, - { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, - { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, - { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/e4/ad91d8297638aa2258aad4501c306aca99480dfe76ccd638173fa3702db9/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", size = 27177, upload-time = "2026-08-20T07:32:44.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/86/5363df11b86d02cf3662208e7406496327649cc90eb365bf6f4e8a54a41f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", size = 26597, upload-time = "2026-08-20T07:32:45.172Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b5/a14dcc592652347dad23ee93b278a4da5d2a25c9ed3ebd10d68eea823a4f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", size = 27403, upload-time = "2026-08-20T07:32:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/b4a20d4902af7f796390bf9245ff83c5217dfa7367efa1d14986956c482b/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728", size = 27132, upload-time = "2026-08-20T07:32:47.13Z" }, + { url = "https://files.pythonhosted.org/packages/7e/1b/c8de358af07b1c490e0fcb863ef98e46ddb486e45567aca5a60bd68d9daa/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81", size = 27588, upload-time = "2026-08-20T07:32:48.087Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/7ee62a6e79f9309f9d9982d301b22a00010adb580c05c8109b94d7b33de0/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4", size = 26785, upload-time = "2026-08-20T07:32:48.977Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5f/f652055e18d2627e2eed94c7f31a792127cfe38df786635395d742321674/argon2_cffi_bindings-26.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083", size = 15434, upload-time = "2026-08-20T07:32:53.143Z" }, + { url = "https://files.pythonhosted.org/packages/76/38/de696045960f5b846d428c0fb6c130ed3da87aac2af209b05c193815404c/argon2_cffi_bindings-26.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", size = 15449, upload-time = "2026-08-20T07:32:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/a8/7e/be212c751ab0bcea7f646615f933bf262e8e50b3f7bef32f861d0a2d066b/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", size = 27311, upload-time = "2026-08-20T07:32:56.166Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ee/f84b28e4afd13d3cac36c1d8fa8c239d2dc2c51cd978d02ee5d5ad98d9bb/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", size = 26771, upload-time = "2026-08-20T07:32:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/21/c3/95c07a023691ecd529da9cb6a8f0779e13ebc1bdfaa86d145fdc1c6e7e79/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", size = 27568, upload-time = "2026-08-20T07:32:58.361Z" }, + { url = "https://files.pythonhosted.org/packages/e6/31/3a18e31406d8694b4d6a31573c3e572fff6bed318bb744453eb653766d22/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2", size = 27280, upload-time = "2026-08-20T07:32:59.343Z" }, + { url = "https://files.pythonhosted.org/packages/0b/39/d4be4577e178b2397aa5b5575c8a309bf0da2afe05fe0c72c8f398662d63/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a", size = 27776, upload-time = "2026-08-20T07:33:00.325Z" }, + { url = "https://files.pythonhosted.org/packages/71/47/78f4dd96f7411339f723b96fe24039c1bd5835102b8a5ba71ac4ec712ac7/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a", size = 26932, upload-time = "2026-08-20T07:33:01.272Z" }, + { url = "https://files.pythonhosted.org/packages/83/e5/02015b83e9b05ccb85ff2ced424cf6e83a12d3810bc7f66d679a92b69ffb/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6", size = 27310, upload-time = "2026-08-20T07:33:06.344Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4a/85e612787d0796878b3b4f6bd53dcd5484b6fe7b64cc6fc7b6e6a04cf835/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990", size = 26771, upload-time = "2026-08-20T07:33:07.429Z" }, + { url = "https://files.pythonhosted.org/packages/f6/84/ccb003b6f9969820e87656398f4d49c857def71a85ca1588a0e809afd7ce/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08", size = 27569, upload-time = "2026-08-20T07:33:08.598Z" }, + { url = "https://files.pythonhosted.org/packages/88/07/c26b76debf0998ee08fbe947ab2058ac5de37d4b9d46b06c17abaa6c4ce9/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca", size = 27279, upload-time = "2026-08-20T07:33:09.518Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0d/ead6ddc029f91bc9b9390686dad3c808ab08100d348f6266b5f93f8970ee/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1", size = 27774, upload-time = "2026-08-20T07:33:10.728Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/c108530d9eb86036b78d3af4de28b83b4a2d9a70512bd10ff8e59966aab4/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36", size = 26933, upload-time = "2026-08-20T07:33:11.661Z" }, ] [[package]] @@ -161,11 +173,11 @@ wheels = [ [[package]] name = "astropy-iers-data" -version = "0.2026.8.10.0.32.39" +version = "0.2026.8.31.0.57.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a4/36/f81b3202279d00e8c7f8f1e9506cfb2ad2b590cb1377caf50888f375fe85/astropy_iers_data-0.2026.8.10.0.32.39.tar.gz", hash = "sha256:2e5b9904f69ca69c7013170b2622b8818d443e10fdd5d501e1c2d298dd1e06dc", size = 1943087, upload-time = "2026-08-10T00:33:36.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/5f/273407b9e1191f8d32f5a383107c5a1a89188cc5ccb9c015555b065bb015/astropy_iers_data-0.2026.8.31.0.57.9.tar.gz", hash = "sha256:4c67cfaec2235b761c11763479984ad4ab25b899b4f9dd66df90225d11a81bf6", size = 1945645, upload-time = "2026-08-31T00:57:51.579Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/27/0775134a2a939ddf7a073e77e66110b671e99cefbc36fb57b57851fd109a/astropy_iers_data-0.2026.8.10.0.32.39-py3-none-any.whl", hash = "sha256:04239afd6e9615165b84da1b84d73f03ca473f0ddf53b516c260e27349a95d03", size = 1998788, upload-time = "2026-08-10T00:33:34.729Z" }, + { url = "https://files.pythonhosted.org/packages/91/18/31d7cd4e8ca5dfcf982e8937fd6069ccc4bee450002c9d9309be115bdd38/astropy_iers_data-0.2026.8.31.0.57.9-py3-none-any.whl", hash = "sha256:c6ac376d845846653b372a68ee5f24de234b92db9179bb82263b85efe4fe558a", size = 2001149, upload-time = "2026-08-31T00:57:50.291Z" }, ] [[package]] @@ -254,15 +266,15 @@ css = [ [[package]] name = "build" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "pyproject-hooks" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/e0/df5e171f685f82f37b12e1f208064e24244911079d7b767447d1af7e0d70/build-1.5.0.tar.gz", hash = "sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647", size = 89796, upload-time = "2026-04-30T03:18:25.17Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/b7/1db48a9ce2984842c8c886432ec8a2719613322e868a966ba82a28862f25/build-1.6.0.tar.gz", hash = "sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af", size = 113825, upload-time = "2026-08-27T21:01:16.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/fe/6bea5c9162869c5beba5d9c8abbed835ec85bf1ec1fba05a3822325c45f3/build-1.5.0-py3-none-any.whl", hash = "sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f", size = 26018, upload-time = "2026-04-30T03:18:23.644Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e5/aa1e81b21aea0ce0ba435311837a37d4cb936e7461f9fecac08580073ba9/build-1.6.0-py3-none-any.whl", hash = "sha256:f7aaf1ebbb79178a02ba248bb524f2176b256017e17e8e4bd4289c7b38cc2bad", size = 31187, upload-time = "2026-08-27T21:01:14.957Z" }, ] [[package]] @@ -284,7 +296,7 @@ wheels = [ [[package]] name = "camb" -version = "2.0.2" +version = "2.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -292,10 +304,10 @@ dependencies = [ { name = "scipy" }, { name = "sympy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/4d/757913df76e3fc816c6e371d8999c5022503e64a048c5669677398401ec4/camb-2.0.2.tar.gz", hash = "sha256:d6b859540b9f8e63bd17786b49f175fc9c0b4ac9ea320772089650b296821687", size = 921747, upload-time = "2026-08-15T19:01:35.551Z" } +sdist = { url = "https://files.pythonhosted.org/packages/51/0b/856e8e329c088155509e3c1a999b02cd3f0333a31fc1fa37653455a4ec80/camb-2.0.4.tar.gz", hash = "sha256:2f1f5f3b3964a3746693dc88a54218dca4ea049a04726f8177664c11d2d2616e", size = 925860, upload-time = "2026-08-25T22:10:58.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/85/522c9a60f794f30a475313761aac40c014c8a50468cf6df4d4b0d6af8c9a/camb-2.0.2-py3-none-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7d122682f03b48e32b714eb98a9e8f582de7764ce693c6cbb7cbe64c700822d", size = 1764778, upload-time = "2026-08-15T19:01:24.896Z" }, - { url = "https://files.pythonhosted.org/packages/08/cb/a9efca3de90ec673a8793e00877fa52c1a067483a9bbd1e3fdbc7a6c37cf/camb-2.0.2-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c4f1cb12630930b3297a8eae31edf516d1f489f27257c23f9161f7f54ef929d", size = 2392555, upload-time = "2026-08-15T19:01:26.588Z" }, + { url = "https://files.pythonhosted.org/packages/35/00/2204636fd231679a9b1ee37a158eb23f273aa3a66a2a4fd81fb1f3514302/camb-2.0.4-py3-none-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:179b8a5496be4c80edf206824a3d211f38d496c0e06420aad754fd16f0bd4fe6", size = 1765545, upload-time = "2026-08-25T22:10:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/3a999a7a500d8a4aef4fa3e3efc94fb00e59cec5e3e0a61973f1cad5b69b/camb-2.0.4-py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b7e6558b2049757e96496b8a33461d8ff11acd79017cfbd00a573dbc8d1e366", size = 2393435, upload-time = "2026-08-25T22:10:49.166Z" }, ] [[package]] @@ -487,11 +499,11 @@ wheels = [ [[package]] name = "click" -version = "8.4.2" +version = "8.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, ] [[package]] @@ -591,115 +603,115 @@ wheels = [ [[package]] name = "coverage" -version = "7.15.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/c3/4f2195f512fb172aa425a8803a874b2baa9ba7f80ff7b6080998761fc701/coverage-7.15.4.tar.gz", hash = "sha256:0548198fff07ccf4faf469520bce1c2eceb1ce3e62891921138dec10907f9d00", size = 936952, upload-time = "2026-08-06T13:50:24.442Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/5c/54ee0d4748585bb0acab9891cd8d92f2d3593165b4e59fc9de113bfb3140/coverage-7.15.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb55d0e70bb15f2e81477613627286581414693d74ac7963c93a790dd453ca9d", size = 254407, upload-time = "2026-08-06T13:47:40.488Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3f/f0642a372f494bd0d7dad3b497083b910194a5f1c88be2c94fef707c3b59/coverage-7.15.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:899b9da30f3c6c336566e3707495bb23e8302d39d862f01fa78c48b99b9437e2", size = 257145, upload-time = "2026-08-06T13:47:41.931Z" }, - { url = "https://files.pythonhosted.org/packages/71/17/8b46d0ed68251016002ec972c8fc0119961a765d0984cafb8bf317c43758/coverage-7.15.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d15715e8c46552827e5e4f30a35575a2dbcad14454cf3284c54483946bd16931", size = 258257, upload-time = "2026-08-06T13:47:43.527Z" }, - { url = "https://files.pythonhosted.org/packages/30/b8/8498a0e72d0adbe15477dd07463d2b3bb2c9f6a4815e8589e50939e2c3ae/coverage-7.15.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8", size = 260517, upload-time = "2026-08-06T13:47:45.121Z" }, - { url = "https://files.pythonhosted.org/packages/41/e1/7dce19c3bdb1e3dd63e769508216500edad81bd5f69a26d724e32aceaf78/coverage-7.15.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4193a04b518f7968f3099755f5509ee7cccc6dc2b92a6b14841934d22e222c9", size = 254785, upload-time = "2026-08-06T13:47:46.541Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b1/e1494703c675a2561723cd9b89f45c9168782c31280c611b1f767851e57c/coverage-7.15.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e98dcc55d572b38e69d117da7e8e8efb8500f1f5eaf81ecd460a63220790b839", size = 256176, upload-time = "2026-08-06T13:47:48.155Z" }, - { url = "https://files.pythonhosted.org/packages/73/76/a5629d270fb638a43a4b10466f51e2f49d532c1aa4da2913cbbb150bbe0a/coverage-7.15.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:af6c538498ce66c10d3fd541c2a8d5b03da5850355add34e6cba564210cb9e72", size = 254321, upload-time = "2026-08-06T13:47:49.757Z" }, - { url = "https://files.pythonhosted.org/packages/ff/4f/9c44447218435d5766b911534f9d798144a5560f85e9a54ebe5f3f5d19f9/coverage-7.15.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d10025d96ea89fc2f73714dbc4cbd433fe012c1ac9e23f895d7728b238b6e52", size = 258390, upload-time = "2026-08-06T13:47:51.248Z" }, - { url = "https://files.pythonhosted.org/packages/de/36/c1e127616fb3fa18a9ff71e76c417f2fd7424332a4870015ac224ef4c039/coverage-7.15.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d802e1947603162ded419bff83ac7489820355d2b856dfb09206574e3a37ac0c", size = 253894, upload-time = "2026-08-06T13:47:52.816Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b9/fdb92c8ae7a8bb9b850cc253b7b3b9c8526f68130002048b5671cd510d09/coverage-7.15.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c2de40895718f91951b86712b4c5b694acaf9a0a49be13874896f599a1eed3f4", size = 255763, upload-time = "2026-08-06T13:47:54.296Z" }, - { url = "https://files.pythonhosted.org/packages/07/4a/612ff1e780b3fbfd637486f542f84adc5503873d8b5d279dec1ffeef9414/coverage-7.15.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5172326e861a38b48b48befca15e0f477a26b283337a33a739c8fed229934e36", size = 253926, upload-time = "2026-08-06T13:48:04.382Z" }, - { url = "https://files.pythonhosted.org/packages/b0/04/d1cff1c2ead4708a6a79c01d3736b6a25bd38a36678398f72a8dd33dfad9/coverage-7.15.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:12b59c90084e3234fb11184886bf4a40f4f16a8c8f867be2e087b81f8e8868d4", size = 256523, upload-time = "2026-08-06T13:48:05.996Z" }, - { url = "https://files.pythonhosted.org/packages/b9/80/d34e13fb4b293cbdb9665838cf5522077b8ad14ef947550631a4bced36a5/coverage-7.15.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349062d66f00b40fa2c1c222438bad25fabf755631b5d82937fe985c8008615c", size = 257759, upload-time = "2026-08-06T13:48:08.036Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e7/2c5fe7636fdb0732fe0f09f308a5b066864078b7fc61f6678e8478554f2e/coverage-7.15.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4256ced708e598e05209bc1a8ab4074e04a51dba4c62fb45926a229af675ace7", size = 259890, upload-time = "2026-08-06T13:48:09.834Z" }, - { url = "https://files.pythonhosted.org/packages/92/28/9689f0858dfff59c2ea688938ab9fa2925631235df67126a42b6c5c70ae1/coverage-7.15.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d80f974b20782d9612c8b4c9beeca867074c7cf4079d1419843fa25a26428b25", size = 254121, upload-time = "2026-08-06T13:48:11.459Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e2/785077c230c157243eb5aa9a26c3be260ecd02001bead54a3cada3df8e03/coverage-7.15.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2e179f19bfe1d31f8eeeaa12990194d761c4f62f0759661000bca6cd8729f40b", size = 255891, upload-time = "2026-08-06T13:48:13.209Z" }, - { url = "https://files.pythonhosted.org/packages/d4/90/e20371b17b40f912f21305c2db2f30efa3de306f7320fc916804872c85a4/coverage-7.15.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8bc16bb47b7679670eceff71d78bfb7d6e5b143f6c2cd117487ec7c75e0d4b78", size = 253859, upload-time = "2026-08-06T13:48:14.736Z" }, - { url = "https://files.pythonhosted.org/packages/05/49/25371987ee459a5f67c0427fb75c74f9358e65f2c71fe75bf41c1b6c5fcb/coverage-7.15.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd685005cd2c4200adfc14cf39a603b9320efab3f18a8f7f156d20c9cc3345f", size = 258011, upload-time = "2026-08-06T13:48:16.464Z" }, - { url = "https://files.pythonhosted.org/packages/30/6e/32e67467f6154bf4f1c4f63b05acc5097cba4237d45bbeeea446b52e8ac1/coverage-7.15.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:337399ad2c93b3acd2a937627dae8b3e86b66707cd3d3e856347999aadf1ef8d", size = 253676, upload-time = "2026-08-06T13:48:18.493Z" }, - { url = "https://files.pythonhosted.org/packages/03/c1/8b24192e89286399765155251f99ee9f070a9d637109018ac23d99b99f6f/coverage-7.15.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96e257121228ec5cd2bb919276e94ac11074471bc37d68dbae0e8308cce15fff", size = 255453, upload-time = "2026-08-06T13:48:20.057Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e1/ff8f9f53d9fcf586125b55d0b1f04ec1c14955fee41e83d5814bee141bb5/coverage-7.15.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5669c8378ebde86f5def7a25d29586631b58acc27ffde04399f678f3dfc6e082", size = 253985, upload-time = "2026-08-06T13:48:29.995Z" }, - { url = "https://files.pythonhosted.org/packages/a1/26/595759762e514e81be1d7d01ed03444303bcd152226a6529998d253f9201/coverage-7.15.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ff97a14362eef486483ed44042ca2027ea257df6ff768e62358ee0c9776925ac", size = 256492, upload-time = "2026-08-06T13:48:31.634Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/b79aabac54d482be23b5fcdd4f4662bff24a78edc4ee29201726929936d5/coverage-7.15.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a325e815318638aed1655d9c06e6d7c2d3d46c09231ce988070428a8762d734", size = 257837, upload-time = "2026-08-06T13:48:33.186Z" }, - { url = "https://files.pythonhosted.org/packages/09/0f/bf7f297885a5bf6fd71e5782404e0ff059ca09e8711ceb3a08544abde45a/coverage-7.15.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:474223409d88eb20d2d6a0d37ea60e8647a65a90cc008dc1f0410af5f64f1e0d", size = 260152, upload-time = "2026-08-06T13:48:34.75Z" }, - { url = "https://files.pythonhosted.org/packages/fd/f1/296744e854ff8368542343457414380465e9ceefb9192342feb9d3bc461d/coverage-7.15.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f2f62ae3cd189dd2e13aece758c57b3eecbd27be070dbd4cbd10936049e5dbf", size = 253978, upload-time = "2026-08-06T13:48:36.434Z" }, - { url = "https://files.pythonhosted.org/packages/55/b0/bbdb2e9057493e66220a2e149ca2d301ba0e3a58a83bd6b90de9826d16f3/coverage-7.15.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39ece820e29e0a2ba34b3ecb3be83c27e997eed8926f2ba6fe7ce7a0bda5843b", size = 255846, upload-time = "2026-08-06T13:48:38.317Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/38015b2b6d21258713bd17e76b59d033b191efb5703589cffd037dfbca20/coverage-7.15.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f21b56dcace11dfe013014201f577dcd592b2a9b72182d930361b47cf6f73f25", size = 253808, upload-time = "2026-08-06T13:48:39.993Z" }, - { url = "https://files.pythonhosted.org/packages/0b/64/0d515c1e60ee6fbfd1a0e79c07cd87d388a233b7adc37758735677203808/coverage-7.15.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:93a3a0b662abcc10c73a47cbc72cd60f63618d6989fb2d1286e50eacd974f303", size = 258081, upload-time = "2026-08-06T13:48:41.971Z" }, - { url = "https://files.pythonhosted.org/packages/91/71/04d9e7a3642146c6351338aef4ef85ab11dbbb54744c13245caba1aad1c0/coverage-7.15.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:141fae2cabf5569b782c10afc4c850ce10f618c13f8db54765cba99cc839da1f", size = 253624, upload-time = "2026-08-06T13:48:43.731Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a7/6c28b74c81ebff66987b0e2522ba5cffa3e90b0c33cb6a2eb264d4ee8cf1/coverage-7.15.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:81294c7e6ab30c5f74c0353b11b2fd6320e72d9bee6ac73b357caa8b916323a5", size = 255280, upload-time = "2026-08-06T13:48:45.58Z" }, - { url = "https://files.pythonhosted.org/packages/82/1a/b1f089da8d38ac612fa2dd6dc7f4a1a7657d12f3e261d2996edd3a838d0b/coverage-7.15.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46822e9b6ff1c6a72b518c162c44a8f45a61a1d609c51084bf5b16c023c5037b", size = 264970, upload-time = "2026-08-06T13:48:56.403Z" }, - { url = "https://files.pythonhosted.org/packages/bf/31/e66d98d6e9c7fcc88470f1e234eaf6b1950dc0dfbf797f7282c1c861da24/coverage-7.15.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6f4955b73b5445271379a59e3792b0d978f42d4a01e0cf7a67d9c33a3bb0a5", size = 267088, upload-time = "2026-08-06T13:48:58.41Z" }, - { url = "https://files.pythonhosted.org/packages/59/a1/ae94eb2c541add426378408379f233591e069040b1e2cdb33df9498a0682/coverage-7.15.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3fc9e047706fb4a9abb54f719d3aa643e80e5bb3818182c40aee01ac0f0247ba", size = 269508, upload-time = "2026-08-06T13:49:00.42Z" }, - { url = "https://files.pythonhosted.org/packages/9c/c7/88a10694a1c6a213569766aba9f25847b28155d4ac731b13226db216356d/coverage-7.15.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05e491d4f3165d62d4f5c8fd48dfeabf2ae8f42cbbd484319af33ea851b78982", size = 270629, upload-time = "2026-08-06T13:49:02.234Z" }, - { url = "https://files.pythonhosted.org/packages/b3/34/d8b8232e5e55169933b59aabcef2fedfa4b9d8897361bb80fcbda146505f/coverage-7.15.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:226c66e80ec0598d3b9b4874123df167ccca342aca8714f77cac6829688ee09c", size = 264043, upload-time = "2026-08-06T13:49:04.102Z" }, - { url = "https://files.pythonhosted.org/packages/7e/35/58b009dbf8c471c7224716478b9fed4a7e1af15320e1ed41660978504663/coverage-7.15.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac41cc14bebda0dbfb0628036b7f75706935c95bcc07fefe9a0f93614aa60a57", size = 266963, upload-time = "2026-08-06T13:49:05.821Z" }, - { url = "https://files.pythonhosted.org/packages/62/aa/57fbda1b42c892968273c56b6ee9dc0f1310850859230a507bc7873b1f65/coverage-7.15.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8af623e5cd92080acddd02b38f2f406a2c3a0893c38950b211890361448fbf26", size = 264569, upload-time = "2026-08-06T13:49:07.706Z" }, - { url = "https://files.pythonhosted.org/packages/98/8a/360e6e7f24d477b7e889703af0afa878d15b6d4d8d2a822b2835c169a879/coverage-7.15.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:07545711d4f0f32852a18f18ad11f76f0109909d09e78b9008b4cfc67e829429", size = 268299, upload-time = "2026-08-06T13:49:09.587Z" }, - { url = "https://files.pythonhosted.org/packages/4e/89/6f701261aee21b6b5fa8f7872229406dc917e125069448292223bf213606/coverage-7.15.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a0865421cfdc53654b342d515e5a233187590882d20b95752150e53f65460017", size = 263413, upload-time = "2026-08-06T13:49:11.604Z" }, - { url = "https://files.pythonhosted.org/packages/3f/0f/6f04036edc260ed425af83e834f627fad48941ce97b50bfe6edd8b6fa623/coverage-7.15.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:460115e32ee40566476db5048f9bec1e842c127ad8e6f8be745aad3ac9cbc839", size = 265725, upload-time = "2026-08-06T13:49:13.38Z" }, - { url = "https://files.pythonhosted.org/packages/99/1f/ef3ffb5557febc75a0d97aa459d0266d7d741110265121cc6d8539343d44/coverage-7.15.4-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d1abebdb047729e852b9c77a00497dfbeb11eb3a117e037d7dbc3ac8e5f5c54", size = 254050, upload-time = "2026-08-06T13:49:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/6f/f5/1f0f6f77698c3601ca0ae7431e34b24c62ca2f06fecb23b73ed1f651d2be/coverage-7.15.4-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d28a4a899354d0ea6214cc59b4fa19eefbce1b9ff1688ab579acf49e894bd3fb", size = 256967, upload-time = "2026-08-06T13:49:26.896Z" }, - { url = "https://files.pythonhosted.org/packages/03/7a/2ed9bed79925f4367c83c77f66a89e5ca7229c288d2d19ad5f36d1ca0070/coverage-7.15.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffb3c2aacea411cc7e1d27712490c11108e2de1d39019ae32915493a59a8b9ed", size = 258587, upload-time = "2026-08-06T13:49:28.692Z" }, - { url = "https://files.pythonhosted.org/packages/45/8c/fa34044f71b7cc4ecb6da9c2408770959b0591fa9b5fb6fb6bca38f94298/coverage-7.15.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9447978a92f405d301123cfd39ff49895490efb769a758fe2734c7f631bf8ce", size = 260785, upload-time = "2026-08-06T13:49:30.472Z" }, - { url = "https://files.pythonhosted.org/packages/4f/54/d5727ce36b4524a7394ab9f5f1df378e1f23affcdab01037dc8655185cc7/coverage-7.15.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c", size = 254545, upload-time = "2026-08-06T13:49:32.271Z" }, - { url = "https://files.pythonhosted.org/packages/dc/e6/6e3783e576719590194bdffb6dd6d85490801785b7c331e35a245d8cb8b5/coverage-7.15.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d003b7a5708ddad5c206c79607a6b92abb6fc13c57d99d8a4468cc03a2941ced", size = 256682, upload-time = "2026-08-06T13:49:34.089Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/bacdbde18b69ed2de424fcf64d9fb0a4913753d4f0eca8bae9daad69f4bd/coverage-7.15.4-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c38efe30fd74e5c19e9433f11fb1f5dc9c6522770971b7c6145bbaa413dc8800", size = 254560, upload-time = "2026-08-06T13:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/6c/a3/1fb927196e3477c1b48831169ab58ba08f451ba87ae311ff1de68b26a616/coverage-7.15.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:1f4f826d70f772ab8b0c052329580d7fe8b8abd191e4ce0c8f81aec6614665d3", size = 258792, upload-time = "2026-08-06T13:49:38.01Z" }, - { url = "https://files.pythonhosted.org/packages/41/58/30d4c149c69053de0edfe325614c1d28d508f62b1783e0e4a234d2e49136/coverage-7.15.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4a4bf917c9953f57c957be31c1cd504e3bd2f34d4a352b9d391a3025336f6768", size = 253968, upload-time = "2026-08-06T13:49:39.934Z" }, - { url = "https://files.pythonhosted.org/packages/89/e4/77f639371b918aad30dda4051f95404b43578f7f2e2f87ba73e02ed1ff37/coverage-7.15.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1c9bf40ebef178a45192c75c4964760bb261b0e6ad725da5fc4c93f674f19753", size = 255893, upload-time = "2026-08-06T13:49:41.825Z" }, - { url = "https://files.pythonhosted.org/packages/80/4f/152b8a4779ae90da11bb24f7467df8a59f0be48a5c52acb856325ca48289/coverage-7.15.4-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4e6f6f632b7b2f714bf7a1346e8f97b650ee71f3c298aaad42a2ab60f0f07645", size = 264489, upload-time = "2026-08-06T13:49:54.52Z" }, - { url = "https://files.pythonhosted.org/packages/10/2d/84b4b9e0e1dd6528a51920ff7031f35b789382e467a28ec6a5a578cb8812/coverage-7.15.4-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8df457da2249d3c75ca2e5e835d59c725abfe92d27fdff6cd99eed85b51d5e9a", size = 267567, upload-time = "2026-08-06T13:49:56.721Z" }, - { url = "https://files.pythonhosted.org/packages/53/fc/ba01cc25299f9f8a2c8b02d3b28c53f3543d9fbfbe4e74fa2760b48f163e/coverage-7.15.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624", size = 270123, upload-time = "2026-08-06T13:49:58.736Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d0/db2647cbf40b14f8c308f94ff7bf89c06d564e59f396906edf50086ec788/coverage-7.15.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1587fb771d1ccceef708fdde1e5af8c7ed24b486b61d13a321acb7d8145390aa", size = 271107, upload-time = "2026-08-06T13:50:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4d2d17924552c458bb4f77dd631f0e3bc92fbbdf2d2d916cd4b33bbfd5b1/coverage-7.15.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b4f1c3a69ca580f3fbd6b2046915f536d7f586874f25c1bb23add2a3c88d50f", size = 264955, upload-time = "2026-08-06T13:50:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/ee/de/dc010c7a3691f396d93bbc26bfcafa1c2a3a351cd520470f15faf5795bd5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:ffb58d7eff5b7f6ecc6fa21d6288ab7f968a212cb67d682c269c09b9eba3b66f", size = 267949, upload-time = "2026-08-06T13:50:05.557Z" }, - { url = "https://files.pythonhosted.org/packages/78/ea/dc96a11375e83c045c2f7c61fb6918277cfe9401db7c0f7b1d111a84b2e5/coverage-7.15.4-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d9df165544774574ee004b953023d1bebada1894a80b1052a43d798b0f676e67", size = 264421, upload-time = "2026-08-06T13:50:07.612Z" }, - { url = "https://files.pythonhosted.org/packages/c8/86/b77131a0f9503ce461cd577076147d7a9040f0c5dda772686f729e2cc9cb/coverage-7.15.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:f9de0a24a4079b53e523b5c5e2c5945ec251ab486652659955187cf255a259bc", size = 269121, upload-time = "2026-08-06T13:50:09.58Z" }, - { url = "https://files.pythonhosted.org/packages/24/24/944bc35007862955e7ebf05754e645419dcf5d7526c52735cfa2715e8ebf/coverage-7.15.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:150089274bdc9f940628552cb92844e0223c987f1902ab8efe9f45a2ec758d88", size = 264565, upload-time = "2026-08-06T13:50:11.722Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cc/a3bb9f93e7e740659163e2ea584f8196ddcd2c456a5dbe15f6c50105fec1/coverage-7.15.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a58a94fed5da6997d258e8f7668c1e195fbd04a691d781b7558f1e468f9e68bc", size = 266522, upload-time = "2026-08-06T13:50:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d9/e70c286c979378f061d8266e279b686ab0b0b688e1fe0af864684f23a77d/coverage-7.15.4-py3-none-any.whl", hash = "sha256:964730a1e9de9c0cf11be6a1a3c79ce419c34882842abd256086ba4698705e84", size = 214332, upload-time = "2026-08-06T13:50:22.192Z" }, +version = "7.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/f5/deb1a27aa20746c0278ac998c4179e272004699b2d33959ce020c5ac1615/coverage-7.16.0.tar.gz", hash = "sha256:077f0964087883176ff6ab9b074694cae29f8c708273b13ca62c183c6ed716cd", size = 945620, upload-time = "2026-08-28T21:54:37.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/65/f9d469e97c4554372a710650a109004a2434dfc56f577142e5d6057fa0cc/coverage-7.16.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e701938ec9081d3e400a0c9a9a8ae0f7ca44214741daeac4454b1c6ef6dbd19", size = 255054, upload-time = "2026-08-28T21:51:31.54Z" }, + { url = "https://files.pythonhosted.org/packages/95/29/dd89fd39af1a3b6e9a9c3eddeaf03f6376ba517d43d6cbf8b519177e2a10/coverage-7.16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:719a3feb6220dd32ed932d4c3676d17fb8739e2643b29c0e7c3af400ff80ac44", size = 257790, upload-time = "2026-08-28T21:51:33.374Z" }, + { url = "https://files.pythonhosted.org/packages/0a/64/208d26cedc525d6b5db9c492cf9130784c42d9eb08d22badaa7b806005ad/coverage-7.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87771ecf986cff55e87413238cd5e4f54d949c2074bd6fc1657d26a56314ee24", size = 258904, upload-time = "2026-08-28T21:51:35.096Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/28e2752aa9a8baee5798edade9c95602ca200f4e7eeb503eb64df42e5921/coverage-7.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47d5e1fc0b321c8308a2aacee0497c435b08acaa629b7059798fdf6fc3006352", size = 261165, upload-time = "2026-08-28T21:51:36.744Z" }, + { url = "https://files.pythonhosted.org/packages/eb/77/fa6ae699a0ea2bc12acb38a85d96b786fea0f833c12b5756056350e0e547/coverage-7.16.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01b18b8a6c9cec8d5f45550e2501426ed982cf2c35016b0acd2ba9b5d8b2fb06", size = 255416, upload-time = "2026-08-28T21:51:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/89/c8/5ee46d1de7d34cb00ba08b5c50da1971114dbc09ca9898ccc32975ec74dd/coverage-7.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:32c56b5b47c50635081445ac404dd08c2d591b9c837c22570aa9e182c3b42cd4", size = 256825, upload-time = "2026-08-28T21:51:40.27Z" }, + { url = "https://files.pythonhosted.org/packages/15/f6/d59e1c0693ad48855fe20169fbf6ee5befefe5887a7fabf5f0bcb464a2dc/coverage-7.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6ad3bbad240ab937512156bc944fdee63ac4dd34a7558a3094548fd4c1150c02", size = 254970, upload-time = "2026-08-28T21:51:43.136Z" }, + { url = "https://files.pythonhosted.org/packages/df/7b/b51bbe05b3a7565927fccfb1be42b8b3c1f4ab15e53d91b303e9923969aa/coverage-7.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4c1f16d5555a195295d0dc9c902612270e3dfed6a11f3bf7bc470b7b6a79ed3c", size = 259039, upload-time = "2026-08-28T21:51:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/d513f816456a8a43c1859abe88a37d01d7d2515b6c3e24ebb3c9b1dd44ec/coverage-7.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f6c9c21a8bf0d19788f3c5f3e020c90317a0a63ef60521b376003801e21250fb", size = 254539, upload-time = "2026-08-28T21:51:46.733Z" }, + { url = "https://files.pythonhosted.org/packages/dc/54/5542190ceb97e0d1333a4ce0c8f95b2ef2efe790f1ad018a4b61766f849e/coverage-7.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f20145a9eb5bf1fd1dde3c0bc2af2e7c22135ab07ca6284d6ada7cc3904c4e", size = 256410, upload-time = "2026-08-28T21:51:48.363Z" }, + { url = "https://files.pythonhosted.org/packages/b9/40/bede08621b1ba67e88c4d3336c22b52cb7911ff1fa4ef055344b6670e58a/coverage-7.16.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4080ad6bad9f14690e6b2104f5e8d137ccc65a4b5427a36662090637d4bd16d5", size = 254575, upload-time = "2026-08-28T21:51:59.233Z" }, + { url = "https://files.pythonhosted.org/packages/12/d8/ab0bdaa45dfd6b8cbf1a3ec548fdf827684b1997f9724375c5b3e89144fb/coverage-7.16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9883a2f8206ce3af59117dc278e5d043fea06912bca3f199816129e5e2de354", size = 257172, upload-time = "2026-08-28T21:52:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bb/135de81784bbd7dfedcab2b92b03d71d75b09b0815b42d6dabb052def5a6/coverage-7.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984e5430fc6f858385009e92549955157d79335b1f3e13e1031e0f89d1284261", size = 258410, upload-time = "2026-08-28T21:52:02.76Z" }, + { url = "https://files.pythonhosted.org/packages/ad/72/ce44ecc062fb2e43d9447bb76154d091c2139232f20c125297c4b58f4c6a/coverage-7.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b1374099dd1ad0d31fbb6c95d00a56a3c5e85fb3343dca14fc12f78323a2b42a", size = 260539, upload-time = "2026-08-28T21:52:04.821Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/9389c36a41e59406ca2bba493807c2294d2e5186a7e9ebcc2e63a0f2a711/coverage-7.16.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34d8686bce035c8465b318a8c2890e69ba14a00801a27f4eb6bdc97c23944d87", size = 254756, upload-time = "2026-08-28T21:52:06.68Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0f/7762447b15e01fb84263608540123c4d9941f06303265ee74d801ccbec0e/coverage-7.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:857fceba6ff4b507ee0ad98798a33d544a8473df0c542bf04251ee4ed5ee6292", size = 256540, upload-time = "2026-08-28T21:52:08.529Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fa/c60dc75a8346c1dbebebc7279b19971c88f70dd575f0bc10bc0cb16f92d5/coverage-7.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bbf08d951abaa1ce89e28c998361d56b952413846b459cd017f116ad4c9adbfa", size = 254508, upload-time = "2026-08-28T21:52:10.323Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/4e0834f3a1fccaa8bf625a2a1d73bde0fa32577dc3249853c0dd0e7f2b20/coverage-7.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1a03e78f53e4d2ab13adac19958a89322d1829913e5623d642627bf60b35da21", size = 258659, upload-time = "2026-08-28T21:52:12.124Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ec/fe712d3a11fd6e874565a5fa5497c48b8ece561d9611da040b44cdcf8386/coverage-7.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:dcd3dafcdd78305d27c59a1006b53a4990acb89e68d8fbe0992f4f83503c827f", size = 254326, upload-time = "2026-08-28T21:52:14.181Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/093e12072e01034c65ff380f76c74b79dd83e44fa92b689a2154389be734/coverage-7.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1bcfe470a796fbea6234accd81d258a31574dc0b7bf569e16be757572c4de17", size = 256102, upload-time = "2026-08-28T21:52:16.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/be/298f2456230fb44e272a4e53a41b3f3c39f0821c242d7b7daa9787b4d6f7/coverage-7.16.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:35a9676bf86097f790113ebd9fb67681804ef54d40941d2f10ba68c02239e575", size = 254632, upload-time = "2026-08-28T21:52:27.689Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9c/a1bda6439c19c4783d50df896142b67b9e7d432db36675d339a32778669d/coverage-7.16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f98d438add63546745e5e847192e3e9ab897ed6f2ca96f8281e2f5a15958ae62", size = 257139, upload-time = "2026-08-28T21:52:29.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/cd735c9be757f97237c305f36897a5e5b348bdbc12ebed3b2b80060dd8a9/coverage-7.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:151855767480be14db595cbc2040f6a4db965cdfeebd354d79b0256742b029e0", size = 258484, upload-time = "2026-08-28T21:52:31.68Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/84b2e1e8aae9db3f549782f28ce25bba5fd6a9c7bfba3782ffe8b4cd2559/coverage-7.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:183613f664718b340589d7f005c7e92b4b601cffd20a8a4117cfda3e983b080f", size = 260798, upload-time = "2026-08-28T21:52:33.642Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/e04cf52483619a4dc5dd6367b30c9a8ac52243567fdfacec9b11a441565c/coverage-7.16.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:785b114356c99c0dd5b3f57b9696cfd57b7704f4c53847df8dc88c6cc0d9bcb6", size = 254612, upload-time = "2026-08-28T21:52:35.543Z" }, + { url = "https://files.pythonhosted.org/packages/da/33/627c4113f66bfffd43807f54dbf080c4632ecf12e4ef7a3bdd4ec38e46a2/coverage-7.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:30f5aee6d1d517abcdfd4f9cad027969ff79a1440a22da263f9514e31b5b66e9", size = 256495, upload-time = "2026-08-28T21:52:37.485Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/aaca432f4e008a88f2bc4d1459aa7016d8d1bbbe801f7e4fa3cf2746557b/coverage-7.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:190ffa0f5af966254c249fb3aeaca2cef389785e3e287fd577d39e134d20f8a3", size = 254454, upload-time = "2026-08-28T21:52:39.425Z" }, + { url = "https://files.pythonhosted.org/packages/cc/db/8430aa87ef0a508f4c17c1b8fa7e0cf80231988d9081aa36c194036592d6/coverage-7.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ccc37c00e1a5d30840902c54557e104d04aead872cedf6d2281c8725a467e06", size = 258728, upload-time = "2026-08-28T21:52:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/76/88/cd8aa8c82493ffbd291d3ef5554452fffc634c6c6098a04ac848c79c98f3/coverage-7.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6c60cde430c0e7e3be612973af39b4cff90ec2e2defe7b2b701daea3a0ffff04", size = 254271, upload-time = "2026-08-28T21:52:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/a8/49/fe16c811ea9314a84b48f34e4bf5a3d9013091093b285a74b2272fc863d7/coverage-7.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c5297028c8df849a61b29129cadfe682f90b5b396f528eb319a57d7678eefdad", size = 255927, upload-time = "2026-08-28T21:52:45.461Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2f/51442e6ad9d705369596f08496021647e276d5b57311818fd4312d93509b/coverage-7.16.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b7dbbbf6551eb94618e7bc76ab61cc2740a5b3d13294171bd6adb36e12346c3c", size = 265619, upload-time = "2026-08-28T21:52:57.645Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8e/0f752276f6d13efbd019ab6d90792e20d6272c44cda039dc5c6d27b91e7f/coverage-7.16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51e7d0e311d2fba3915f971236cbdd4ad821fc7a23988221c0b33c964b0eba22", size = 267734, upload-time = "2026-08-28T21:52:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/02/4df3baef8029881c9d1a380859f2be73f90080d430def567d182e8566a35/coverage-7.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bb04ee77e557d7476471969d35fbbfb5fc8a4152e9409aa5811780c36d9b23e", size = 270156, upload-time = "2026-08-28T21:53:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/9f/30/ce10fdb74055ebbfb5c8a025d8845dc19c76e4b2c42bb5c755b56678990c/coverage-7.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c72c9b201dc0e8c2c8821d49858fd865010d08181bf877d2320971b6464ebfd5", size = 271279, upload-time = "2026-08-28T21:53:03.698Z" }, + { url = "https://files.pythonhosted.org/packages/71/19/c7e1fc9504d90da848493bad4018dd235c713a80633e48c5f0a41b63d45e/coverage-7.16.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0fca700cae4635656668ba6e2b66a85aac9f2622d7b2bcf82e844c409eaa1313", size = 264677, upload-time = "2026-08-28T21:53:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f3/4021519dd41583ab396c81955387f927779641f6bac26818b6918a45aafc/coverage-7.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:584896fb8b650e999e24ef57e9513e482c12f8e15a73ee9d4584e23c99465867", size = 267610, upload-time = "2026-08-28T21:53:07.763Z" }, + { url = "https://files.pythonhosted.org/packages/55/fc/df65aac93938d8f506434c8e96440c1d696f6be0a6a01d3c6bfe5d49403e/coverage-7.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:949eae7e0f562b1518355aaef4b03523e49a6d3fea12aa3542d9e36c863f8267", size = 265217, upload-time = "2026-08-28T21:53:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/32/2d/dc9a5e62715165fcb4c715f965f411e324917c9daeddde16536e9d36ce3f/coverage-7.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:64f0611ee05364fc85cc3e5bc371804117a76fd337720e6017332fc7c534257a", size = 268948, upload-time = "2026-08-28T21:53:11.866Z" }, + { url = "https://files.pythonhosted.org/packages/8b/4e/fe73a5560f25fca52acda76fc1554f30de081793ae4de97e920f8ab161d7/coverage-7.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:050a291b3cfe5e0df5999ef2fa5a7aff6e2db329f069d47eb63f02bde2e7e96b", size = 264061, upload-time = "2026-08-28T21:53:13.996Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f7/bb78cc4b97085ebbd77fa18cbc25abfab462814efa3e2363b4e50885c775/coverage-7.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a336b1e2990a64f5c356a9b8380fb9c029d56c832b801255250c44d603271bfd", size = 266371, upload-time = "2026-08-28T21:53:16.233Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/38e93a10899c9315964c0a4e729b3e5867f8f46e977808f9c6fbda52525a/coverage-7.16.0-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:acadbf2f2a18d7f9c7f119ac798c00c540d7c79c93abd71ed648c87891303633", size = 254699, upload-time = "2026-08-28T21:53:29.715Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7a/acddda030b4630f68167f3daa94b41d22071847822a70d8178d43dcf678e/coverage-7.16.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4212cec9b42fd9929e70b462732fefd8b13406371871c82f3c14397499d6550b", size = 257614, upload-time = "2026-08-28T21:53:31.948Z" }, + { url = "https://files.pythonhosted.org/packages/15/7e/225b182497c1ce6d3f0d76a3074a4dbc9f272300e92bb100df53b03de0aa/coverage-7.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c5a43cc0ef101637ae920a9eed24cf0549ef815621eae68b3ad577ec5a7ad2f", size = 259236, upload-time = "2026-08-28T21:53:34.291Z" }, + { url = "https://files.pythonhosted.org/packages/2e/19/76641ddc50cb2410ebbd0ed7fe1052614d0e5612e802a2817521adb9febb/coverage-7.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c76a9b50a344261fe4a9bd20c322b48d3913cc48e8c37f78c21a596008296e68", size = 261433, upload-time = "2026-08-28T21:53:36.401Z" }, + { url = "https://files.pythonhosted.org/packages/12/9e/5f89de8b7c2017f36b68b4e4a25940723a748b21474820bf61e8bce0891c/coverage-7.16.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80cf547379ad6b1878fd03b033b51188beab4b41824c96e7839e014a4cb947be", size = 255182, upload-time = "2026-08-28T21:53:38.496Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c1/ce94b2ec502e79775efb5efa22c741ebb0bd2be10bdd29650825ff57bdcb/coverage-7.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4b1d09cb5d8dc2c7164450f5217e6f0717497de9c588806a0780d352abef904a", size = 257329, upload-time = "2026-08-28T21:53:40.87Z" }, + { url = "https://files.pythonhosted.org/packages/86/8d/3f5374df3a6ca19ee5f98a6bd21dbb05f1e9d399bd9978e9821d260eab5e/coverage-7.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:cd1e85abed2d2499c16664137ac802356316f92b4e2bf3c150bdf0c45f5dd9ae", size = 255210, upload-time = "2026-08-28T21:53:43.393Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b8/1bc5751496d0be6fd9dde8ca547d9a8a9f07847856aba3f3ae5ac594cd81/coverage-7.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:360967a6fd77794c167529eec2d16ff8e38216110619d23acc3fd466a1648bee", size = 259442, upload-time = "2026-08-28T21:53:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/dc/8aca78e47e1e6fcc761cd28a20daf4a84bd847a7369e2701a93ccfc3d1fd/coverage-7.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:92cbc2bf4f7f67c79f1d3ca4fe8c50faddf48e852a3d07eaaf02dc014889832f", size = 254618, upload-time = "2026-08-28T21:53:48.292Z" }, + { url = "https://files.pythonhosted.org/packages/73/fd/787842cdf6ce16ac5c1bd8a26549bab3b3f27b02500075bc540dc7853bca/coverage-7.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cce4dc8528453128c6fae523b15f3887fbea1d4d7c9eb9639d3d4fdcbe570c73", size = 256541, upload-time = "2026-08-28T21:53:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/f6/39/10dbc96d95d20b9b041045d293480bd49e536180e93af62dd7662376284d/coverage-7.16.0-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b83f6ac575530783771c8dcf05284f7c8b5b12f1e7cb226d63445aac4497a3a", size = 265135, upload-time = "2026-08-28T21:54:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/6b326544afd1a8aef3a495bbae109a7ab5baf23e04a2741d8d64e2df2ba2/coverage-7.16.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c3ff6580f2dfc5bec34717b85b2e6cf5ec993b721e7bb58a794babd525a8178", size = 268216, upload-time = "2026-08-28T21:54:06.97Z" }, + { url = "https://files.pythonhosted.org/packages/54/34/1dc8265f3ed990690e24d5f31ff79bc9fb9b25d54f9f89bebad5a6a8b7a1/coverage-7.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507596cee23e9968b1934fe86d799b76166541af0a293930918b1b48a5c84bd2", size = 270772, upload-time = "2026-08-28T21:54:09.234Z" }, + { url = "https://files.pythonhosted.org/packages/66/a7/3a8463713a402b44044ec832f4a76e442ce4b3a207804303f4d1dc1a9bb4/coverage-7.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edc2be98e6c55ccc5ff7832bb64f023a4b03dba39dfa84b850046cf08a8249b0", size = 271752, upload-time = "2026-08-28T21:54:11.701Z" }, + { url = "https://files.pythonhosted.org/packages/25/3b/dd5e795cfbe1842f69899189089ae289a96d6a68de312960ea668542e33c/coverage-7.16.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c0690994b84a15a53bdd39e0b2fdb539b22533820623eb86ba75b93760c645b", size = 265589, upload-time = "2026-08-28T21:54:14.12Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b6/fd90636cbd95cb018312f6ca1ca2bbd70fbe8e4ee6f3992fc36a4230364e/coverage-7.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:de24c62bf798940a14674a47489a81b79915ec4134f556d5199830e065225dd0", size = 268596, upload-time = "2026-08-28T21:54:16.303Z" }, + { url = "https://files.pythonhosted.org/packages/91/10/ef2d59264f3b3b358cc5885ca375e6cdbda7c195e78304d5aae800a72d9d/coverage-7.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:69474d81f198774c9d2937599ca5da04c9e1c5de5032da23c607ce4960ce360e", size = 265072, upload-time = "2026-08-28T21:54:18.597Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5b/400891c364c0170408d172501b340b18611800f4c42d8fbb16f9f5497c24/coverage-7.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:72a0795cc6d34acc2b03dfeabdc82b61b72087f2737018b56ac92c1cf5446c54", size = 269768, upload-time = "2026-08-28T21:54:20.985Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/9792c80271df04d287d21ed5d662fd8fa58b1737888d817679b1ce5d2fab/coverage-7.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:d9a218d3f9c7d6916684ed5ba94f620661117a730e733cd6ef5e87accc5872eb", size = 265211, upload-time = "2026-08-28T21:54:23.344Z" }, + { url = "https://files.pythonhosted.org/packages/81/67/5b8f827cfa6616e6bd7ba9397acfe7e3c4fd5b9fca4125511d5089f55d5a/coverage-7.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:49fa72ead28c8216f8916398a4f3c4669acb30a061822810ee20a727a1be2897", size = 267170, upload-time = "2026-08-28T21:54:25.85Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5a/234e8fadf85c3cc48cb31c247b9e8e0c7f06ece80f5b29f9b8c241f9da4c/coverage-7.16.0-py3-none-any.whl", hash = "sha256:245f7de6d023a5bba375dbec9f2e0869bfa26ac0cc639bbb7b4c814884000b73", size = 214977, upload-time = "2026-08-28T21:54:35.189Z" }, ] [[package]] name = "cryptography" -version = "50.0.0" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, - { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, - { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, - { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, - { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, - { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, - { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, - { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, - { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, - { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, - { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, - { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, - { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, - { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, - { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, - { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, - { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, - { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, - { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, - { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, ] [[package]] @@ -732,7 +744,7 @@ wheels = [ [[package]] name = "dask" -version = "2026.7.1" +version = "2026.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -743,9 +755,9 @@ dependencies = [ { name = "pyyaml" }, { name = "toolz" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/39/cbd21c9133d02b4e60899ed466ad5e876553ea68ebffee6209e7dcafc8d4/dask-2026.7.1.tar.gz", hash = "sha256:5727484427665f051e86bf87d021a64d6411141cdc8a20bfe3c1ad2968cc06b7", size = 11548794, upload-time = "2026-07-14T01:06:22.46Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/a7/6b3c7ac32b642fbbe0821111654e0bd8cfbe88f68560bcf23cc78ab35c71/dask-2026.8.0.tar.gz", hash = "sha256:8a94c37b5de6d869343340dc26c3c3acca7ec48a3abdabe00ea3abb1125884d5", size = 11561752, upload-time = "2026-08-24T19:21:25.906Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/5f/7c22733da92b3a6cc4dddcaa8731089d213c2790bbc997e51c429a4e8f8b/dask-2026.7.1-py3-none-any.whl", hash = "sha256:985ffd6c5e9d7979ede515e84ae8d39b647d6aa64f77600f15714ff65f578fe6", size = 1496882, upload-time = "2026-07-14T01:06:20.341Z" }, + { url = "https://files.pythonhosted.org/packages/f8/3a/4fc99e788bcfa1b3b3f21abf57da45898d807d007e7f6fd1c7300904eb70/dask-2026.8.0-py3-none-any.whl", hash = "sha256:ccc0c83a189b0398602435189771d28dad7b5773b6089bb8dce14ae732dd782c", size = 1492182, upload-time = "2026-08-24T19:21:23.997Z" }, ] [package.optional-dependencies] @@ -968,14 +980,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.59" +version = "3.1.61" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ca/dc/126b28e76b24a9268ba931ad3e012f71ebdadf62fd9f17758f7074bb0b20/gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4", size = 230445, upload-time = "2026-08-10T12:03:20.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/61/3285044215fb596bf093e39ccb96ece0a1076a8ca57a61e069a6a33cdb1b/gitpython-3.1.61.tar.gz", hash = "sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95", size = 231680, upload-time = "2026-08-28T11:01:13.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/ed/ae57eb7d344f43f87b74b3a281ead6ec7d6394eef72a7b1dcb28dd089550/gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c", size = 220996, upload-time = "2026-08-10T12:03:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/49cc172da4d0578644ba37cec5cb365b1fefc603b26edea9bcac1c7f830a/gitpython-3.1.61-py3-none-any.whl", hash = "sha256:8ab28c9da863cdd9e7d7694ec46cf3e6c9a12d8a30a1acd3447aec11975d530c", size = 222118, upload-time = "2026-08-28T11:01:12.262Z" }, ] [[package]] @@ -1105,15 +1117,15 @@ wheels = [ [[package]] name = "healsparse" -version = "1.13.0" +version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hpgeom" }, { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/33/36520faabae128fe9a4a5cbeb0916be864d2eb34cf7410d52dc532af044e/healsparse-1.13.0.tar.gz", hash = "sha256:c21e4eb075c686bd306bdc1b40e039134c4ca60ccfa4e7e73845c107a5b16544", size = 133589, upload-time = "2026-07-07T18:33:05.481Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/d6/c299485e7bc16779e33824d838395a7339565d193e017ca9aecbfc0a8269/healsparse-1.14.0.tar.gz", hash = "sha256:60e94c8a12ca3af80cfbba7b7b11c30be1c6b5585132dd9f8ea93c7935887198", size = 133778, upload-time = "2026-08-21T04:33:43.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/2f/7674b91031a12a9b3cd69f3bb675d3b1eac59657139e23bc528ed6d33d0a/healsparse-1.13.0-py3-none-any.whl", hash = "sha256:3b475a61d7701526246a592ed980758f94764caba4355429bf470a2f9b812964", size = 70673, upload-time = "2026-07-07T18:33:04.514Z" }, + { url = "https://files.pythonhosted.org/packages/32/f8/e6a566c8b8b02a6cda81da4e0dabc3c673042fd140a0774977c9d9139e0f/healsparse-1.14.0-py3-none-any.whl", hash = "sha256:0328188d15020fcf029dae6e8fee7bb3c168a495c1d266402d9b7776f369eb7c", size = 69262, upload-time = "2026-08-21T04:33:42.123Z" }, ] [[package]] @@ -1288,11 +1300,11 @@ wheels = [ [[package]] name = "idna" -version = "3.18" +version = "3.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -1310,11 +1322,11 @@ wheels = [ [[package]] name = "imagesize" -version = "2.0.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/5e/513ff06670c84e7b9887c1fdf61b2d42b4f574a831f2f1d2222023049d8a/imagesize-2.0.1.tar.gz", hash = "sha256:b2ba6a4dea487a7ebcd53248d3476aca449d30db12a2dde5e0c5ca9624fd77e5", size = 1883774, upload-time = "2026-08-24T12:35:19.13Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/575c8d760eae1fc99651b7cc5efd96ad5379ca4d6b53750b0fb4fe983f34/imagesize-2.0.1-py3-none-any.whl", hash = "sha256:ea0c9a0384df69ed86a943a15cde37d0360b82491b3910dc2215e202e62b5b02", size = 14794, upload-time = "2026-08-24T12:35:12.548Z" }, ] [[package]] @@ -1335,14 +1347,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "9.0.0" +version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zipp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, + { url = "https://files.pythonhosted.org/packages/b3/55/ecca97ae19075f1fac62def77731e7f535e6c1fb8f92ff08160c5e6dade8/importlib_metadata-9.0.1-py3-none-any.whl", hash = "sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0", size = 27920, upload-time = "2026-08-28T15:30:33.433Z" }, ] [[package]] @@ -1569,7 +1581,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.9.1" +version = "8.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -1579,9 +1591,9 @@ dependencies = [ { name = "traitlets" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/dc/5512503b088997c2250b8bf18258fba9d9ce5ead641183700960d3c9d342/jupyter_client-8.9.1.tar.gz", hash = "sha256:a58f730dd9e728ba16ba1d62ebccf7ffe1ebbdbce4e95cfae941b7321ae1f4fa", size = 359256, upload-time = "2026-06-09T13:15:01.033Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653, upload-time = "2026-08-28T12:17:10.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/6f/56d39bf385c5c27988aebaf0c18a2a17e960575740100973511018bd904e/jupyter_client-8.9.1-py3-none-any.whl", hash = "sha256:0b7a295bc46e8751e9adae84781f726c851c1d911bd793edc4a3bde942e3da81", size = 109828, upload-time = "2026-06-09T13:14:58.835Z" }, + { url = "https://files.pythonhosted.org/packages/0f/88/7c548de1f6c2ade7c931a3282da73f9274fa6a1531091be682f89c85efb9/jupyter_client-8.10.0-py3-none-any.whl", hash = "sha256:5f73f24f22fa25192cfff6b23c051932a2473a797b05734aff495b392103e14e", size = 110184, upload-time = "2026-08-28T12:17:09.028Z" }, ] [[package]] @@ -1630,7 +1642,7 @@ wheels = [ [[package]] name = "jupyter-server" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1651,9 +1663,9 @@ dependencies = [ { name = "traitlets" }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6b/dc/db3a582633170186f8c8b31298d7eb26ad0eb031a1f53476c258b64eed05/jupyter_server-2.20.0.tar.gz", hash = "sha256:b5778ba337d8015a3dc2b80803ecdd5ac18d3797fddf61a50ea5fb472b4ebe14", size = 756523, upload-time = "2026-06-17T12:09:09.435Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/e0/63b481d5b21f81fe23173dfc9eb25629fa1bc174fdc4a2c19d09c1ff8124/jupyter_server-2.21.0.tar.gz", hash = "sha256:70d9a1883f57d3576ea17f4ce061ec1a7aad7ef388d00428cfb7f5e4f0022271", size = 760357, upload-time = "2026-08-27T16:06:34.049Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/71/8c002223e873a870f5c41dc69b0a7c922301123e4a31d5d01ecb700aef77/jupyter_server-2.20.0-py3-none-any.whl", hash = "sha256:c3b67c93c471e947c18b5026f04f21614218adb706df8f48227d3ee8e0a7cdcc", size = 393143, upload-time = "2026-06-17T12:09:07.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8a/cb97c2b353cb46cced413be8f742fa8fcd3cb1659524f27314494aa94968/jupyter_server-2.21.0-py3-none-any.whl", hash = "sha256:2ae2e5ce5e97268553e25aebe040197455673d925b5fc7995477153318160cf7", size = 393961, upload-time = "2026-08-27T16:06:31.814Z" }, ] [[package]] @@ -1737,61 +1749,80 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, - { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, - { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, - { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, - { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, - { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, - { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, - { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, - { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, - { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, - { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, - { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, - { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, - { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, - { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, - { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, - { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, - { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, - { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, - { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, - { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, - { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, - { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, - { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, - { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, - { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, - { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, - { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, - { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, - { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/bd78e6a8fae171ea041ef5bba3ed21a003522fa088834b069b1909981f30/kiwisolver-1.5.1.tar.gz", hash = "sha256:f1303ef2eec81262a4b708c3e858afe58d7c75ad91c1c05266eda7673369859a", size = 104395, upload-time = "2026-08-28T10:28:27.153Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/f4/dadfec469313c7f428efa7e84b4aba9732f813c13ea7131a24b7b008ef57/kiwisolver-1.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34633ecf50d16187ab8e5528b7a2530f2feb4e23f300db4672538b51cfc5cd38", size = 1477929, upload-time = "2026-08-28T10:25:31.495Z" }, + { url = "https://files.pythonhosted.org/packages/6f/35/09c58daac34e6f6ea5c6dee0094b422118e5a7c265586008a95fd135ac5f/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d27c2123977cb9269c30a49ba45f03a4323017ef693e19db4ec9dbe1299a3002", size = 1278499, upload-time = "2026-08-28T10:25:33.375Z" }, + { url = "https://files.pythonhosted.org/packages/19/32/739765e24fbad29d13f83e546ea4abc215a78cea9d677ca09025b027724d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6a797a1cefc8b9c93170db580337e1fe3d011ad18b1299943231279406342048", size = 1296677, upload-time = "2026-08-28T10:25:35.059Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/03304d1010e2cc45e5b3b52cef7e43fed3a2a5cd6c87a89b4a88e1d85b5d/kiwisolver-1.5.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2551cf9917af48ee7c4b29cc82320489508cf96fd26a51f6fc124de661cd44c7", size = 1346037, upload-time = "2026-08-28T10:25:36.705Z" }, + { url = "https://files.pythonhosted.org/packages/3e/57/4c49377bfd274450dd72ecaa13eaac32ea804a03363e4d1db0c5aa999ceb/kiwisolver-1.5.1-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:38f6e0deb4d0a4615efe0c4efc5990b06ae450ab50a0b321c0b078b6d238c083", size = 988248, upload-time = "2026-08-28T10:25:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/38df144a08b6c5d75ca4504e5cc3141bb3bfef64c04f4ef48204f42711b6/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bfd1de989b3330420e29de39352f5c049905c9e3ee67233a50d550e3d652c148", size = 2228722, upload-time = "2026-08-28T10:25:40.038Z" }, + { url = "https://files.pythonhosted.org/packages/e7/11/3221838a89cd64d9b386353e000cd8a296069a20fbe3584507fdfd5bebae/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1209042a623ddfda5497e4066c7b77651dde8e1d3a9dd97599dc7e97f3b9b78c", size = 2325216, upload-time = "2026-08-28T10:25:41.699Z" }, + { url = "https://files.pythonhosted.org/packages/83/d4/075c219230697bb5db910d37262b9bacf880f92b4811a02ab81ed073a253/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:26e8268480be5061d509e29669d59103c067a26377a56491630ece11762e3858", size = 1977689, upload-time = "2026-08-28T10:25:43.559Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/1d219c3c2dd960983d0d4da623d916e9de6385df2b0bab3d1af0e9b8fccc/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d79308fa689fac89cbcfbd4dbfc80b5f95c54c5a7fd4d194be221f9d33d026e6", size = 2491443, upload-time = "2026-08-28T10:25:45.242Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d3/024208ec1079d273f1047468d1bdffbf38bb75b7b268090fd3a0301b9d9a/kiwisolver-1.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b03af77d77e50edba2030fd5f7c352ff209314b09030a3cba7c14edf9a09a444", size = 2295200, upload-time = "2026-08-28T10:25:46.984Z" }, + { url = "https://files.pythonhosted.org/packages/08/c1/88018321d976f53c421e379c43bc6993e70ce0c8a3ec5edc4bfe102257f6/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b6ae6a0328f0bc035741820fdeecdcd67bf4694eee03972e843663107122f450", size = 62272, upload-time = "2026-08-28T10:25:51.02Z" }, + { url = "https://files.pythonhosted.org/packages/85/d2/712bc17ea4f1d216034928069d612defbc6c95a471c55a7203a39faecb1a/kiwisolver-1.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:886fc26012f0e8b5f69d1cfe6d711f6b11f194621539bf8e6bb1c25c5dc82724", size = 64481, upload-time = "2026-08-28T10:25:52.22Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e6/6c5380d676f43b6d918033962ea5e72360ca69e5a404154bc496b598ffdb/kiwisolver-1.5.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:aefe930d113798330e9462f7874542977869c0613cba3262e2de3a8d5dee8f3a", size = 66260, upload-time = "2026-08-28T10:25:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/ab/43/86aacc027959108b4c66eeae8b73cedb057dfa6eb3a335d05ad65197081c/kiwisolver-1.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:74ad5c3dad54a4641b4c28cd15ded70899d04459c6c7aeacafea716be97cce6d", size = 1477992, upload-time = "2026-08-28T10:25:58.479Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/b1d0369048c79733a32c8abb0f2718532e6630641368e33a81384246e844/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e46b23a2da695c364124817bc01d970effd5483147f8d66a6a7167e3f6b851", size = 1278821, upload-time = "2026-08-28T10:26:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/61/a1/fa71c1792272ff9461432715ae60ffb7e11a4d7ac3bf68961b9cab6c60cf/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75d9b1cf8258462dbdc1eeda718c96ea7f079324c09067f6daabfcf37712b7fe", size = 1296805, upload-time = "2026-08-28T10:26:01.868Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3f0bd94af8e06ecc47eb834b195a06f05d48711ceb2352c56d6835160f0e/kiwisolver-1.5.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fca690b00c4c48f6c2a547b0160ed511357093a4e4c9b47e0fadf3128066d89", size = 1346109, upload-time = "2026-08-28T10:26:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a4/44/3afe6ef9cf06d61220953a8963e94eca978491be1d9547cb01d82a1efa08/kiwisolver-1.5.1-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:876bbfd276473d3daffe30e8c975df4ed9429967b41a6cb362dbb5155b6f13ad", size = 988252, upload-time = "2026-08-28T10:26:05.306Z" }, + { url = "https://files.pythonhosted.org/packages/e4/30/a12bd7a7285a211e1747c3eec77b8c614dbfcc1dad942f7611a1a6921ae5/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f942903fde7363d1d879057ec5de01310efda2597161784d752fa9953a01a71a", size = 2228846, upload-time = "2026-08-28T10:26:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/233e2958abf0dab7b18d07e52f286e02e519d7651bfbbe97af9347564109/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c90d3022d8a94778939cda8638c6c8da8fa757b8958dad7ec868ce29c87681b8", size = 2325583, upload-time = "2026-08-28T10:26:09.093Z" }, + { url = "https://files.pythonhosted.org/packages/42/8e/7673060a27b01405b580058510adef34687069d229800239f5e44682d4d0/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8a34616dc2521cc8dc1d7d081734da63539f021ac0450ce950908340c6e7aa2f", size = 1978221, upload-time = "2026-08-28T10:26:11.127Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7b/1b882fc1a8b4a0bb8084e7d1d85004116d08c92c0705d31f2928dec607f2/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:8bf4df63592c2a66b4f8edc5df2544998c288aa02f96ce0acd880cd1de8c8127", size = 2491819, upload-time = "2026-08-28T10:26:13.348Z" }, + { url = "https://files.pythonhosted.org/packages/39/9c/426deb49e62c5f69464b64bbeca064d3b758a7506b8913d986ef34f4619c/kiwisolver-1.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d09037ca068d784ebc4aec290ef952ca27ac15dd9c0b5801a88c6e1096b83e6b", size = 2295520, upload-time = "2026-08-28T10:26:15.042Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/2d5dfad0daf17dcc18d98c48ed2332fc3f051cf599e60be6182a30dd4cf1/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0324cd2567259b7a095f6cf18a52b0ffc6f3de9e69528ff1bc0e7a37bd43ff1a", size = 62337, upload-time = "2026-08-28T10:26:18.778Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/83e1624f15d6262b470dbcc80b09979fd4d5b2ea3ddfc6b6e3327e235726/kiwisolver-1.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:74ea337e0ec3f6f342a36a4f1b5cd94dd9affddcd28ba9aae2905af932ee8c6b", size = 64513, upload-time = "2026-08-28T10:26:19.909Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2d/827ec30eb07f528c08d8459ffb318ae91a56d793ee8acbea8b491f0ff906/kiwisolver-1.5.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ee9df1f0d77b9c6e94f4ac0fec533fbddd5ea3a327807f18d7b069ae019ded80", size = 66287, upload-time = "2026-08-28T10:26:21.078Z" }, + { url = "https://files.pythonhosted.org/packages/a1/13/08188f0cafa3a800403e4ff62b9aad4e7a17f9c4c7e080dc8f18c64794cf/kiwisolver-1.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:18a0cfb124546a4c2e6087c5f3029c7f44b37c85b142e0ced71f73a7599ac208", size = 1475867, upload-time = "2026-08-28T10:26:26.393Z" }, + { url = "https://files.pythonhosted.org/packages/8a/3e/053bdc3c9abdb8f2606225eda398adca25c0c91ab90add8222a69db65ee0/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34ec467940442c9943016fb2d4c81d1ba84351eeca2f1a78f8bc87f1ba0d414c", size = 1282865, upload-time = "2026-08-28T10:26:28.118Z" }, + { url = "https://files.pythonhosted.org/packages/5e/64/a44c341b36b610588cc2f1e89b3cae072a3119aa8be578e90987cd640751/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a83ee7107df13abe42a54a6654670eef9bb39425cf2e27f65e0007465e1286ab", size = 1300865, upload-time = "2026-08-28T10:26:30.125Z" }, + { url = "https://files.pythonhosted.org/packages/60/5e/7e7d716dca38c714478b741257a5b4a321d9932b8d851551a136dcaf3984/kiwisolver-1.5.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bebb89489b279b2f5661bbbb2abcc87bcd4a46607bb4a5c966f04f1db6b8df9a", size = 1348071, upload-time = "2026-08-28T10:26:31.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/1a/2b98fdda8bf45b7be317e48ed12393d44334394d315c74b81f4a14c0e31b/kiwisolver-1.5.1-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:509735237ae0d849e8a843551d423d2500d2e0a9ac1611a145658b29c0fb9f85", size = 992191, upload-time = "2026-08-28T10:26:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/d893f5ede0e50f9e3fcf01f6015f42ec7d9cd221e26772701fe4a98745f9/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:824c3d763a05ea9e9003610145186b0e9848c7584a5575c79bac5a8e7cd80bad", size = 2233854, upload-time = "2026-08-28T10:26:35.282Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/f85f6279555a6ee1639fef7bfe83adb037a03e11a6fc9eaa54b8d0380339/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1fff05e239575b1481b6ed1a782f6fad616efbf1f0b1f44e6e85c4dfe426e483", size = 2330621, upload-time = "2026-08-28T10:26:36.9Z" }, + { url = "https://files.pythonhosted.org/packages/56/31/e11aea078f66fc2fffcc179d38ca90d9da97652a241b64519169742ba46a/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0627b9bceb9c3cdcf12b8a18655eedfed2692b038df27423383c120d0b7dc2d6", size = 1982848, upload-time = "2026-08-28T10:26:39.01Z" }, + { url = "https://files.pythonhosted.org/packages/af/ea/2956b63bf5140ca46aa2c2818e6aa03e2d5754dd2fa41db1c6b28922940c/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8a708a47ade1fe19e8371d5da076bac0dd4b0a5a7985ad6c637f7f7e361b6baa", size = 2494850, upload-time = "2026-08-28T10:26:40.837Z" }, + { url = "https://files.pythonhosted.org/packages/11/d1/3829542258d8b3fc0898d221e7ef0e2c83eca0d348709bb8dbe54f3d4005/kiwisolver-1.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:007a5553dfc4f4e8d184f588a0200e2cd4b63a59cc8796df3c39909e679dc7a0", size = 2298067, upload-time = "2026-08-28T10:26:42.803Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/17dce073a6ae259bb32cf9d686c4079d2e538868bc45967462bf33df914a/kiwisolver-1.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43844c1a7ad6d723d5b5b4c4fc7f5bd399c40e288120d16257c7c9e8765c6e85", size = 1584907, upload-time = "2026-08-28T10:26:50.933Z" }, + { url = "https://files.pythonhosted.org/packages/8c/84/ae3c75909f507283cbfcc7e916c7e822579ef962020b97e6882b27b4478f/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5e5aaad6be121f2515765e3b1c444352cb8eb4c86510801db8f2e50757316", size = 1392474, upload-time = "2026-08-28T10:26:52.638Z" }, + { url = "https://files.pythonhosted.org/packages/34/31/8bcc83caad5bce8fa4577152389848bf6bc110e51e573a2b4e7c2aa34c89/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3fa5855898f6d3d01b72ccd48a2d65cbdee301251603fefe34e2025bddba219c", size = 1405246, upload-time = "2026-08-28T10:26:54.248Z" }, + { url = "https://files.pythonhosted.org/packages/55/72/220345537d790cf4ae54f8acfff4b5cc2468e0702a384d651cf7a771c63e/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d66a64dd5dec136040ec2ae94aa026a912ee60fdd45bc28d3db30037fd809e88", size = 1456099, upload-time = "2026-08-28T10:26:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/cd/10/3725fd2398f66d18c34b4e0f81a8d03764cd4f4f089f58a527f0b4428086/kiwisolver-1.5.1-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:9e51c119992ea8820706871c30a4642ec76de20ae82f9b50b9a45517d8e9f810", size = 1073695, upload-time = "2026-08-28T10:26:57.658Z" }, + { url = "https://files.pythonhosted.org/packages/86/cb/28d6e09e66b93e4588b2e6b7d84d020ccefea09e2f4de788510a07efeab7/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:70ed9a45c7484d2b30cdacf60d220f494a1763b9fec1ad03285c6553fa0889f2", size = 2335355, upload-time = "2026-08-28T10:26:59.202Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b3/a0f31d5e4e40af7dc97c36b8a74fdd3a36cf3c8bbd098da9a23466ff6a94/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:98b208a7cc42c803445ef551d6753cc42a5ea13e9cab1ee66cd8b9cb70195330", size = 2426524, upload-time = "2026-08-28T10:27:01.181Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c9/728f63bd58c72cafdc79fc306abeeac7391bec03b757a48dadeb30906521/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c6834b92dd2428e2dd85ef3d85f723d3c12f20aaf43a2ddd4f944ca25d833408", size = 2063430, upload-time = "2026-08-28T10:27:03.06Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/ce188b96f92f9a2c958231da140768918cba53c9713dc887b82f85462118/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5d142e352eb13facc7dd047489aebdff6ba78576c239f1ea04931979caaf0567", size = 2597513, upload-time = "2026-08-28T10:27:05.072Z" }, + { url = "https://files.pythonhosted.org/packages/69/d6/76947c8203768968382e5bd74d9cc95654746703a61ea53015f2c74a2e06/kiwisolver-1.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9b1c4900736e489a812c529100de4b8fb617d4db075e931e213c57424b83d9b", size = 2394488, upload-time = "2026-08-28T10:27:07.423Z" }, + { url = "https://files.pythonhosted.org/packages/75/62/f786c3a27f181fa339d851a77e266d208e776b9883cabc40a5b041a31b5a/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:bb7c99f0673c03017a3ee01e54a5c2617a05468b11eabe513b0080e063ed95b1", size = 62420, upload-time = "2026-08-28T10:27:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/02/cd/58a91ed25fbad0facdf503297b03768efab04bdf3141e5e3b49a34be7443/kiwisolver-1.5.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:0d8924877ce22e17326a99a418c3c82037da078df3c6a260b13eca677444e6e7", size = 64577, upload-time = "2026-08-28T10:27:13.502Z" }, + { url = "https://files.pythonhosted.org/packages/f6/5c/d501ef5a0958b226eac28306d24d5e5f114be0ace50e19cabae7b6b3b197/kiwisolver-1.5.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:534f02c1abb31ed6dbd3515545285c330b2f12d00fdb1fdb71658b9ca5a13a6a", size = 66284, upload-time = "2026-08-28T10:27:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/ba7b9c0164ce1cf62bf2872db63b8483289cf0f3110d6f9390eb09e409ed/kiwisolver-1.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca7f6fe0f37ca978a1e5eb7a3a68e6413f417e78e838324947ffd420202b198b", size = 1482211, upload-time = "2026-08-28T10:27:20.039Z" }, + { url = "https://files.pythonhosted.org/packages/13/dc/34da54efb4976616d45c20aae32d70e89d6e7395ed908029154d1609ef22/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b973887ff782cfd6b67c9904ad8ca542e0bc5e4961503408b423b5a688b4d38", size = 1283739, upload-time = "2026-08-28T10:27:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3a/30ffb62bee646e266e98a1b5cd276d9c75b6116fbfcb87c1190838c1b6df/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f76fc85bd054c806960f917ec0f329e24e436f1712267d90588e4c39890caa63", size = 1301681, upload-time = "2026-08-28T10:27:23.876Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8d/13c70be22a8506880b35fdc38dca36629613bc493405c79f4037f2cd2bb9/kiwisolver-1.5.1-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:828f75af2b0080c8a972e75f649ab46af008e92c6104a57a759157200b835b75", size = 1349159, upload-time = "2026-08-28T10:27:25.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/0e/993972b8ec6767f47cd69818fb3a5ff14510557d29f7d1a839be7574fa1b/kiwisolver-1.5.1-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:431dc224a1a92a5c8f582d96e505196a3b5997a7271076678da2dfde67b77e9a", size = 997613, upload-time = "2026-08-28T10:27:27.507Z" }, + { url = "https://files.pythonhosted.org/packages/36/82/ca26eddd2eda2420dfc56693449c1f821f78b485da9cbde9904c03af3f93/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:61e9a64c7635095a6bfe483e2ff055d437c59bd45f3617a228b37277f0185d62", size = 2235109, upload-time = "2026-08-28T10:27:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/c7/84/97d920881e10840b8d7c7185620298e3e4c88820b05514e3a15a258b08a6/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3c24cd69455e1b00ddf770c13b6e2c33e07d6dc3f2d34add0bf9277c5c6bbd46", size = 2331207, upload-time = "2026-08-28T10:27:32.795Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ce/34d74b8f25acc58800f4c09268371e8d6159cf0f1206f1e4dc7835629b48/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:27add358abe374ebaa3b8763ef380bc99051b5a4b18d94878366a9e4f59efef0", size = 1986696, upload-time = "2026-08-28T10:27:34.628Z" }, + { url = "https://files.pythonhosted.org/packages/0e/01/f892644014612527aef7031d3306a2ffc60b3cb044f802c1561f8e5e14f3/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:255605693a483db7bd5c79f60437f7bf658f7f520d61aa42722e32257c941951", size = 2496400, upload-time = "2026-08-28T10:27:36.818Z" }, + { url = "https://files.pythonhosted.org/packages/32/6d/d8284e66e697026536e5f418b9cfe56567bffd3c775e3ecfbae373605854/kiwisolver-1.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:7d38b0c279c3032e8c9cc013b405c6df8e1668dbf15465779aa7f15f61201812", size = 2302967, upload-time = "2026-08-28T10:27:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c9/51dc974d9130da70a8c47a96160123443d387ffe1b6b833d6f91d9429339/kiwisolver-1.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cea90547bfd93807e0013a004dc76552be44fad3bc1cc2b38610a9e889ed098f", size = 1588030, upload-time = "2026-08-28T10:27:47.678Z" }, + { url = "https://files.pythonhosted.org/packages/e6/86/f3e1a730e7a995149d8d3ff9e313b6d8a17b2cf1d98a8eff139dc30463fb/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8e4d953faaded9ec7ede36824e9814082d22d4c7b1eafbfa079ecba8cd0d076", size = 1390760, upload-time = "2026-08-28T10:27:49.676Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/8ba25a2b5a0d2375e046f1b72de5179513f0be95aba6e7b094c89303929f/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e9c01d3dd7ceba4d1d436cc021d40d592466e40b9bc7f5d83dc4e98a5c9cd8c", size = 1403279, upload-time = "2026-08-28T10:27:51.242Z" }, + { url = "https://files.pythonhosted.org/packages/18/d0/278d5cb8be812740027d5ca0a7eda0c375488a88d6dce0fa60fcc2591ad2/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f801b5d7cc0e5a548921308e059fd2b057bb42972b591cfa3049f95423c4ed", size = 1454429, upload-time = "2026-08-28T10:27:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/bcbab41063ec284c1d200efe5087cf087798c2f8916960aa8a20dd303290/kiwisolver-1.5.1-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:e68e151428b5384f766cd25739bf77c7e4a3dc93b5ded7a12118d9fbfdf78ab6", size = 1073225, upload-time = "2026-08-28T10:27:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c5/ab79dcdf5ae28909a51210ae0a1c579e97ff997b3466414f0d04c0994583/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:8f8fddb8e323bd6eee4e54e69a39243beab22689070f4c66b472c4cc88bb89d8", size = 2334335, upload-time = "2026-08-28T10:27:56.78Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8e/71d047468a189041d9c93f3b76844b924f9793b188c44bd149fa258912da/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3cc210010fd2f438a3ed430b45f1b501fd13a8618bf984dc2c5ce5b69b78752e", size = 2424982, upload-time = "2026-08-28T10:27:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/1347461bbea6d0e1f0580b94ef603b18e72c2be5f667fa1653867361a00b/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b5664603a253efd3a75716d793d1d3a6a82723b61dc6db767b2460bbbeec4c0f", size = 2062857, upload-time = "2026-08-28T10:28:00.407Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/c7f0976c9cd0d127643351bc0c9929e0b8899d7f49d4ec238cd909e39c42/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a7b85b2cc6ea45e5f7e8c9a30bc9fabd47cda09106cbb4b967335c3e6c43b69d", size = 2596022, upload-time = "2026-08-28T10:28:02.183Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/59a6f6ae6f938c15076be2c21b6cedea973d71bb1349ec84fa485fab82cf/kiwisolver-1.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ab620eb663952455271ac37f9aaad86b73c969c02f11f53cea405b38e96a4300", size = 2395634, upload-time = "2026-08-28T10:28:03.977Z" }, + { url = "https://files.pythonhosted.org/packages/16/45/c37a21ad5c0ab581a93c55ad544721aaa1f0ae94edb29c6a678a23d013e6/kiwisolver-1.5.1-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:072bdb15a3c19a5b5dbc8f8fb1f4e1884bf4f3507eeb4cc6334401274d37a5c0", size = 194292, upload-time = "2026-08-28T10:28:11.06Z" }, ] [[package]] @@ -1864,62 +1895,82 @@ wheels = [ [[package]] name = "lxml" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/3b/aab6728cae887456f409b4d75e8a01856e4f04bd510de38052a47768b680/lxml-6.1.1.tar.gz", hash = "sha256:ba96ae44888e0185281e937633a743ea90d5a196c6000f82565ebb0580012d40", size = 4197430, upload-time = "2026-05-18T19:19:06.424Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/95/bb63f0fd62e554fe078e1fb3c8fe9083c14ddc7ad7fa178d10e57e071ac7/lxml-6.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c921ba5c51e4e9f63b8b00267d06566e1f63407408a0496da2d1d0bfc819c7fc", size = 4930746, upload-time = "2026-05-18T19:18:29.637Z" }, - { url = "https://files.pythonhosted.org/packages/eb/99/0013e8d9b5960f4f041cf0b73e2f80c23eb5205b1f7bfb20203243651359/lxml-6.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:54a7f95e4de5fb94e2f9f4b9055c6ba33bf3d628fd77a1d647c5923caa2cdcdc", size = 5093723, upload-time = "2026-05-18T19:18:34.168Z" }, - { url = "https://files.pythonhosted.org/packages/29/91/317b332636bfc7bddcff828d41b3307f50043f4b237e40849c333d80fa1a/lxml-6.1.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f2ec43df44b1f76249ee0a615334f9b5b060e1c8bd90e706dad2d14d02f383", size = 5005557, upload-time = "2026-05-18T19:18:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/42/2f/cc9bf06afe70f9c9093ae60855d9759da9db601ec4080f7473319666ffd7/lxml-6.1.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:70ef8a7e102a1508f8121aae5b0867abd663f72c14f0a9c937e6554cb4587b7b", size = 5631036, upload-time = "2026-05-18T19:18:44.858Z" }, - { url = "https://files.pythonhosted.org/packages/08/f6/af32e23e563971ffb0fb86be52bc5be5c2c118858ffc119bf6a9039b173d/lxml-6.1.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebe6af670449830d6d9b752c256a983291c766a1365ba5d5460048f9e33a7818", size = 5240367, upload-time = "2026-05-18T19:18:49.217Z" }, - { url = "https://files.pythonhosted.org/packages/78/83/8555d40948b09ce86f1bd0c68a7ac31d07b1929f92cc1b074006c97ef2d2/lxml-6.1.1-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:27acc820660aaffa4f7c087f29120e12980f7779d56d8492d263170111284740", size = 5350171, upload-time = "2026-05-18T19:18:52.779Z" }, - { url = "https://files.pythonhosted.org/packages/63/75/5d92da93729b7bad783689e6496049fa40927b45bec7bf183c981de3ca70/lxml-6.1.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:1db753c9115ec7100d073b744d17e25e88a8f90f5c39b2f5dd878149af59671f", size = 4694874, upload-time = "2026-05-18T19:18:55.139Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b5/3aad415a9a25b822e783f15deeb4dffccf5113030f1afa2222dd929313d9/lxml-6.1.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4f469aebd783bb741c2ecb2a681008fd26bfe5c16a9a72ed5467f834e810df2", size = 5244492, upload-time = "2026-05-18T19:19:01.28Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a1/5fcf7eb9904b80086aa47dcf0027de07b1bb990afad2e6823144c368ae04/lxml-6.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:766b010012d59470072c1816b5b6c69f1d243e5db36ea5968e94accf430a4635", size = 5048232, upload-time = "2026-05-18T19:18:12.67Z" }, - { url = "https://files.pythonhosted.org/packages/77/74/1f601b63c7a69fcdf10fa9b148c81da8442204194f6c55509cc485c786b9/lxml-6.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b8d812c6011c08b8111a15e54dd990b8923692d80adf35488bee34026c35accf", size = 4777023, upload-time = "2026-05-18T19:18:15.928Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b9/7a78f51aec95b1bf780d78e12705a9f6533284f8693dc5c0e6724fa53d3f/lxml-6.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe0306bd29505a9177aac19f1877174b0e7422c222a59f70b2cd41633448c3dc", size = 5645773, upload-time = "2026-05-18T19:18:23.223Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6e/98a7b7ad54e4e74fa1f20fff776913980619d0ebe5558232d7da6580bdd8/lxml-6.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5ba186ad207446c65d3bb3d3e0412b032b1d9f595e59861e2354798c5703d955", size = 5233088, upload-time = "2026-05-18T19:18:31.433Z" }, - { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/6b/55/a0c72851dfee5ecc689f949723a73dea457758912542cb955b108eaf0d8f/lxml-6.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:762ff394d5bd56da0cf034a23dcce4e13923f15321a2adfa2ac00201dc6d3fca", size = 5082329, upload-time = "2026-05-18T19:19:09.728Z" }, - { url = "https://files.pythonhosted.org/packages/f0/b6/0608f7d61a3b96cc67e5648a3d906e31a5082093e10e7be65b3886289938/lxml-6.1.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a088f287f7d8275a33c07f2cac6c50b9319309a0200a39e7e75d80c707723099", size = 4993564, upload-time = "2026-05-18T19:19:13.608Z" }, - { url = "https://files.pythonhosted.org/packages/4c/66/ae227524b066d29d55bf0b453d93d2d793c40218657d643dcbbca13b8faf/lxml-6.1.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e902da4b04e6b52e5893900d4b8ab46068f75f3561f01bf1080957f9fd932ed6", size = 5613467, upload-time = "2026-05-18T19:19:16.228Z" }, - { url = "https://files.pythonhosted.org/packages/a6/76/dbe4a00b50385e40194231dcfe5a12c059de7cf90e89c83407d2b085b719/lxml-6.1.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d4962d4c66bf830a7e59ed6cfc17d148149898a3aefa8ec6e59763e6e3ed085", size = 5228304, upload-time = "2026-05-18T19:19:19.354Z" }, - { url = "https://files.pythonhosted.org/packages/1c/01/00b1b8442ed2041793336868ba0b9ea4b13d7da7c085c6404c207a63bf79/lxml-6.1.1-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:581d4c8ae690a6609e64862dd6b7c2489635c2d13907fc2b20f2bc200ff1d21e", size = 5341607, upload-time = "2026-05-18T19:19:22.297Z" }, - { url = "https://files.pythonhosted.org/packages/63/36/1ad29931e9a4638bb707869f01d423a6c815f82152138d1a40dfcfde2b95/lxml-6.1.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:876e1ff5930ed8bf295ec5ef9a8155e9b6b1876bbf1deed8b3a8069311875a8f", size = 4700168, upload-time = "2026-05-18T19:19:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/3c/d1/a9536cecf9be18a0dc72d32bead283a2332d1ffebd2dd3ac70ce444686e5/lxml-6.1.1-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9eb9b5a968f6e0f6d640092a567e14529ff8cea2e29d00da6f78a79fa49f013c", size = 5232487, upload-time = "2026-05-18T19:19:28.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/77/b4fb1e03bf5d130e879214d3100092e386418807fb74dd0adc4b0a48f351/lxml-6.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:aa49e06d94aba782c6a02eecb7e507969e7e7a41b267f1b359bb35585f295d5b", size = 5044231, upload-time = "2026-05-18T19:18:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/26/4c/d00daeeb0a5530c4028a9232aa1b93db3ef4ed2158c116ea73c79a9765b3/lxml-6.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:70cdfd80589d59e43e18005dd7244e8895e93db8ab6a620b7e23df5445a4e3d2", size = 4769450, upload-time = "2026-05-18T19:18:48.013Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6a/715a3a8d156ce42f29cf014706f5410c2ff3b02267774110fc23266409fe/lxml-6.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:aad9aa39483ed8ec44d6d2e59e5b98a0d80676ef0d92f44bfc374836111f62f5", size = 5635874, upload-time = "2026-05-18T19:18:51.914Z" }, - { url = "https://files.pythonhosted.org/packages/45/37/0544bc21dde2a88f3a17b504e6fc79c0e01d25a33c2f6079724e9e72b9c7/lxml-6.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d49514be2f28d895c38cf9d2b72d7b9a07d00314519f456c0b50b53cfcf4c785", size = 5223987, upload-time = "2026-05-18T19:18:59.715Z" }, - { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, - { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, - { url = "https://files.pythonhosted.org/packages/94/cd/9c7611a51c37a2830928405817cc5d56a97f64fab83cc3f628748b135749/lxml-6.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efe0374196335f93b53269acd811b944f2e6bdc88e8894f214bd636455484909", size = 5086695, upload-time = "2026-05-18T19:19:34.764Z" }, - { url = "https://files.pythonhosted.org/packages/da/d6/24e3b5906abb0b674ff2ae195bc3ce59708df2bcd17cf17703b2d7dd643a/lxml-6.1.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac931cdc9442c1763b8a8f6cd62c0c938737eafc5be75eff88df55fc73bc0d00", size = 5031642, upload-time = "2026-05-18T19:19:37.771Z" }, - { url = "https://files.pythonhosted.org/packages/2d/db/6ec54f99019838bff54785c51da07f189eb4676861c5f2730962b0d8d665/lxml-6.1.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aee395f5d0927f947758b4ec119fd5fc8ec71f07a1c5c52077b30b04c0fa6955", size = 5647338, upload-time = "2026-05-18T19:19:40.553Z" }, - { url = "https://files.pythonhosted.org/packages/42/3d/ef4dcfffd22d27a61805d8ed9f7fb888495bc6aa88648fa07c1eaa5586b6/lxml-6.1.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9395002973c827b3ed67db77e6ec09f092919a587022174554096a269378fb13", size = 5239528, upload-time = "2026-05-18T19:19:43.657Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/37fb3f0dff146bdcfa78eec47879273820b2a0bf350ec236ce14bd0b1c26/lxml-6.1.1-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:73bc2086f141224ebddb7fc5c6a36ca58b31b94b561e1dfe8e073e3270fad1e7", size = 5350730, upload-time = "2026-05-18T19:19:46.307Z" }, - { url = "https://files.pythonhosted.org/packages/90/42/43253f168388df4fae1f38c01df36ddb9bee39e2048167b54cdcbae85ea3/lxml-6.1.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3779def59032b81e44a5f70096ef6bf2082f8d901937dca354474ba09782e245", size = 4697530, upload-time = "2026-05-18T19:19:49.889Z" }, - { url = "https://files.pythonhosted.org/packages/eb/a8/c5a8504f81bbdfc8e7094c2c850cdb4ed6777fc4d5ddd9e5ab819f3b0d54/lxml-6.1.1-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:86c89b9d55ebf820ad7c90bc533410f0d098054f293351f10603c0c46ff598f5", size = 5250670, upload-time = "2026-05-18T19:19:53.199Z" }, - { url = "https://files.pythonhosted.org/packages/77/b7/c7e76ab18744d75e21f320ebf9ff9d1ceae2b54dd431ea5a64caf26c9672/lxml-6.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19607c6bbff2a44cf3fe8250abccd20942d3462473e0a721d01d379ed017e462", size = 5084485, upload-time = "2026-05-18T19:19:08.422Z" }, - { url = "https://files.pythonhosted.org/packages/31/31/b35c53f8ef7b7c31cacd23d3638652fff7bcd1deb6eedb709ab43b685908/lxml-6.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c6ed5141a5c7507cf3ee76bd363b0d6f801e3321adc35b5d825a23115faa5465", size = 4737635, upload-time = "2026-05-18T19:19:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/d9/06/31f23c813a7fe8e0cb1b175e915b08c9bf4e86d225b210feadbdbe519667/lxml-6.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:62aeb7e85b5d60320b9d77eef2e773994e2c0ce10121b277e0a19804e1654a5a", size = 5670681, upload-time = "2026-05-18T19:19:15.001Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bc/ce619bccc89b1fd9ad8a8e1330ee3f3beff9f2ff95b712d7bbcdd6e22fc3/lxml-6.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b1b963fd8f5caa68e99dfae060d54de1fe9cba899b8718b44a00cdca53c3e590", size = 5238229, upload-time = "2026-05-18T19:19:18.131Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, - { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, - { url = "https://files.pythonhosted.org/packages/b0/02/55fc057d8283427dea7d6edb102e7a840239c77a64a983d92f62a304c0e9/lxml-6.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4f0dd2f01f9f8a89f565d000e03abcf0a13d692a346c8d22f628d49af098777a", size = 5102822, upload-time = "2026-05-18T19:19:59.223Z" }, - { url = "https://files.pythonhosted.org/packages/e4/48/8e1cf78d89d66850121d9255a2a24414c98f775da93b90cf976956c24b14/lxml-6.1.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b7e8a14c8634bf6f7a568634cb395305a6d964aeb5b7ee32248094bed3a7e2c", size = 5027923, upload-time = "2026-05-18T19:20:01.549Z" }, - { url = "https://files.pythonhosted.org/packages/ed/00/0632a0647612c8af24d26997b3b961397daa9d5b2581444805933629a4cb/lxml-6.1.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:86281fbdd6a8162756f8d603f37e3435bfa38043adb79c6dc6a2dfee065e7525", size = 5595843, upload-time = "2026-05-18T19:20:03.93Z" }, - { url = "https://files.pythonhosted.org/packages/bc/86/ab008a7dc360711b66858d61c80a5979a70a09f2aa2b05d9698df80b803d/lxml-6.1.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5d7152ec39ca7c402d8fb9bad86140a15b9503bd0c54484e3f1bbe3dd37ceca", size = 5224515, upload-time = "2026-05-18T19:20:06.381Z" }, - { url = "https://files.pythonhosted.org/packages/75/c6/2702ff375e728e34f56d9a45339a9cf7e4427e917f542225242d63a05afa/lxml-6.1.1-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:88d8cb75b9d82858497a5393e3c63cfbf03035225e4b35a49ed7ccb151e4dc0e", size = 5312511, upload-time = "2026-05-18T19:20:09.308Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/a5807c98f87a86f10ef9ffab35516df7c0f0c4b6d5d33e9f608ab9c04a31/lxml-6.1.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f64ec5397ea6a41fc1b4af0380d79b44a755b5531dcaccd9940fb260dca93038", size = 4639206, upload-time = "2026-05-18T19:20:11.704Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e1/8a0a2c35734812395f4da4eaf33748a7e5705bfb2a58b128da764339d5ec/lxml-6.1.1-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d34bbf07dbc7ca5970671b1512e928991fb5e9d95365636c9b2d8b4f53af405e", size = 5232404, upload-time = "2026-05-18T19:20:14.064Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e2/0e6a4dd5ad84d01d99aa7bae7cfefd4a760a0e0f8176818241de17d9b6c0/lxml-6.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:17e0e18d4ad8adbd0399291bc44845b69d9dd68439a3cdebdf35ff902ec05072", size = 5083769, upload-time = "2026-05-18T19:19:23.758Z" }, - { url = "https://files.pythonhosted.org/packages/a0/7e/161f33d463f6ffc1c7679104b65086dea120080d49dde4d238f015aaee2f/lxml-6.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:3ab541146f1f6968c462d6c2ac495148e8cdba2f8347700b2141b6ec5a75bf52", size = 4758936, upload-time = "2026-05-18T19:19:27.256Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fb/2369825e3f6ca99305bf9f7b7085fda91c8b0922a89e54d900974aa3ef85/lxml-6.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2a0217714657e023ef4293500f65aa20fce6164c8fd6b08fa5bd4a859fb14b9b", size = 5620296, upload-time = "2026-05-18T19:19:29.993Z" }, - { url = "https://files.pythonhosted.org/packages/30/90/d61e383146f74c5ab683947ea14dc7b82778838ab9b95ea73a23b60d0191/lxml-6.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:05a82eb6e1530a64f26225b55cbd178113bd0b5af1c2b625f25e5296742c26d2", size = 5228598, upload-time = "2026-05-18T19:19:33.523Z" }, - { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, +version = "6.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/a9/970b8fa0ecc4fbf1dfaed0d89bbc1fc1421b25ec26a2038c91e872dc6c8e/lxml-6.1.2.tar.gz", hash = "sha256:1055241852f2b02068af4a625a5d32c087db193c12251928af2562ecd2239f18", size = 4210626, upload-time = "2026-08-19T04:58:15.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/e0/21fba0fe74d417fbe976903ae6bc77e92cdce01aae7b636abd87756f4588/lxml-6.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5078ff51e6316c0f75ea8127c2cd24374747fb351f62fb93d1761f8ae5a04a40", size = 4939689, upload-time = "2026-08-19T04:58:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/de/e5/ce3e885264fdd0bdcb6b49c1ea1842f94281b39e4ff956099e8d57532c60/lxml-6.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9477e14217c212e6023c994a71a1a349db19b0e10fd5bf189666b281ae63b1fd", size = 5105185, upload-time = "2026-08-19T04:59:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b6/990a8446c488c70fa25681e150de94b7bf2eaaf387e374d195ab3c8faafb/lxml-6.1.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:261d98065326676d7253882db0198d0aa06748d7ee0443367acf10b148273f99", size = 5011863, upload-time = "2026-08-19T04:59:50.58Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6a/f70f41363dae27e3bfd6224b128f5ba150874bd32ca4938552930ffa33b0/lxml-6.1.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0666943ee1576fa890a6dc6316ef42e8241b5dd56f67bc5475acb2ac298c6ca9", size = 5638234, upload-time = "2026-08-19T05:00:00.802Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/a65b64f34d556925faef2c4f14167d58c571bc15a3e1f2bba71138830562/lxml-6.1.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04cf9e3f4ee9cab9d9ba05401bef8668840fa9620fcd4d8e85a2d2fd0b0fa960", size = 5244532, upload-time = "2026-08-19T05:00:07.516Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/471552e015e954fc9d960aa27c3d67ebf489683d03f033399a790417c67c/lxml-6.1.2-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:9429d2371d406344ed1da5b5686d9412e74137c07b0171278368ff704f470ed5", size = 5358194, upload-time = "2026-08-19T05:00:22.747Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0f/bc6248fbec2cc416f102b1267f1567e07510f6fa909bbe8cd2a22d6fb78e/lxml-6.1.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:eff128ffdc093cc6317955934ad9751105d37ed8dbca3ff4ccd751af6be37185", size = 4704432, upload-time = "2026-08-19T05:00:51.115Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3f/cec859f50e63f1fa338fab43d2362d7543e1237f2475960d8ab0769de0eb/lxml-6.1.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ba58574d710b82ead7cbedea01cac3e110bc3ef82d4731519b74a2c11f7cf5e9", size = 5255038, upload-time = "2026-08-19T05:00:58.895Z" }, + { url = "https://files.pythonhosted.org/packages/7c/d9/2ced0cf2967115f92a1b8b3ae6bd18763abc3ebef88c98cf25145fda396c/lxml-6.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52f6d4dff133c9778a24e9a2cfc1608930b15869866171aacc5131b5a418a003", size = 5054481, upload-time = "2026-08-19T05:01:10.096Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/4f07386d3c88673daeec3b8cc09a2a4d39fa01c1fc49009791b0746d97fa/lxml-6.1.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8807998c1023d1e9d60e02500f90e85a0752dbc0b670989806bba87b82dd5b42", size = 4785535, upload-time = "2026-08-19T05:01:18.909Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5a/f4fe3ecbc189f48fba2547c5db5c940a10151d3e86b856a60a533a77e816/lxml-6.1.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2170d0a280c877b6e2dc6738217db947be35dd8cf09ca458b355aa1bab2a9e70", size = 5655337, upload-time = "2026-08-19T05:01:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/f586aa1bf27bfbace2dfdbb704da5c52f0bdece8ee440c8fb4946c940b2e/lxml-6.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c67f3c1278f942e97d8665c2a690324aaea5137de16f056583a21f0ac706177f", size = 5245778, upload-time = "2026-08-19T05:01:45.227Z" }, + { url = "https://files.pythonhosted.org/packages/18/a1/677494bbaef4d6db5e4633af817414f478865850b55c03ae4bf70fa7b8ca/lxml-6.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093fbf547d0f3ca02705381f795a050fbb58988be4aac7f79f99f280c4082313", size = 5267274, upload-time = "2026-08-19T05:01:57.687Z" }, + { url = "https://files.pythonhosted.org/packages/93/cc/fe74d1def7f4fb967c4a825608a074d4dbdbb871b0d6bd59c6ed07d67868/lxml-6.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8326e24ae6c3a6bfb03fa8b4793f9a5d804c125228aa067f652b0428e31b87c", size = 4936196, upload-time = "2026-08-19T04:59:03.477Z" }, + { url = "https://files.pythonhosted.org/packages/b7/ad/b96e6ca926e26726a99aa643602aac7411ecc1731ddb1b25af8cc57edfcd/lxml-6.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7c534ed898413f439b048130011e99a4245ee13d62d431f6b4f7f2484d02a93a", size = 5093290, upload-time = "2026-08-19T04:59:17.498Z" }, + { url = "https://files.pythonhosted.org/packages/d2/84/616f5d3b7cd086fcfba3e5add6fccda67f976c1c753ae9ed7bbd317cb9be/lxml-6.1.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e37fe49fe2d5aa40a2cb1cc8176673ad7de0d124e6f4a509d9318f5979c7871", size = 4998767, upload-time = "2026-08-19T04:59:28.385Z" }, + { url = "https://files.pythonhosted.org/packages/80/88/d5b453a8d083483c9442ad7f5ac5c560796022eb5c80d60b65d75e449236/lxml-6.1.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9b52ea73a37fc64aa3357ff8607801d46dd170506d3cf8253a91a1d91639d4f9", size = 5626717, upload-time = "2026-08-19T04:59:40.045Z" }, + { url = "https://files.pythonhosted.org/packages/71/45/31e5aa4d4bae024908ba1d03480c7425cf027a28b7e5c88d1b7202bd80cc/lxml-6.1.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8b9a92652e75e7731309ea51db5dee892eef414ce70a6ec3441e5d36bf5189f", size = 5232330, upload-time = "2026-08-19T04:59:46.175Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5a/2627912420df8b2d31ba3014da5539f15ec85add01d42048864ffefda516/lxml-6.1.2-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:9088da25ecd609965f838d89fda0465a905b48f4dd90331db9845518f2177372", size = 5347054, upload-time = "2026-08-19T04:59:52.762Z" }, + { url = "https://files.pythonhosted.org/packages/16/86/54ac0f529b22a8f12313726dd49e12961bb46471d9028cc28d2a29408f0b/lxml-6.1.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:0349321a0537d4fdbebb2af06dd1b64676132c72e2ae250de8cdb58f8c43019c", size = 4707275, upload-time = "2026-08-19T05:00:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/3a/42/ffcdc6e4519be90df907cdae7e88409efb25d823ae4de8846f737dae1884/lxml-6.1.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b20440e578d269c5e8a722ab602ddd0f0cedb8b080006b3f936da9991a593d3b", size = 5240071, upload-time = "2026-08-19T05:00:19.604Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/5b1d7ab35f013f1127ec48f3108319f58b65b00d5cb26f215adbe86eadfb/lxml-6.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7766e525282dd38fd89567311323e441996eb958e8e816d16b38f782e3aecd2a", size = 5050356, upload-time = "2026-08-19T05:00:27.968Z" }, + { url = "https://files.pythonhosted.org/packages/b0/57/1cf049d054189b55c8fe8012269234f6602256949b69cd3ba80608a88219/lxml-6.1.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9221442682c27417f10fe11184ea4cce174b25ab52465570b1f3ee3f85f320fa", size = 4780394, upload-time = "2026-08-19T05:00:39.047Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ad/064488a8fa60e639fd773e421a18bf17541d02a95fbf36238ad7c65f69d4/lxml-6.1.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75530642d8471327e691ab9b0513a5f9c77f38871014ceda40f51bb51765c0a1", size = 5645854, upload-time = "2026-08-19T05:03:42.697Z" }, + { url = "https://files.pythonhosted.org/packages/85/bb/120e56f3cf1c149bb3b014278fb86d0a6dd552403981081f0ee0a0a57be7/lxml-6.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:678e35f1cbca98f55107511ee21a60568535c950f3c2371819bd64504c980d20", size = 5231132, upload-time = "2026-08-19T05:03:45.466Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2c/7d49aab893c128671a3276580074cce4c002896145b8dd2893da79633bca/lxml-6.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c2bae42b3a09f977330a08f4a8fe72aec58c4bdb89069d3fe7272a71d885881", size = 5256076, upload-time = "2026-08-19T05:03:48.092Z" }, + { url = "https://files.pythonhosted.org/packages/20/86/80bae4e8bc2eed9d6f017701a3d86fdea56936218efa738911d0b76aa7f4/lxml-6.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d858e718b94033ab4b67e4a58fe3114c65bae01ae2314a62fb39ae8897ed4324", size = 4964846, upload-time = "2026-08-19T04:59:08.59Z" }, + { url = "https://files.pythonhosted.org/packages/70/ce/4782caee7a22959c1ac67cb46495e03912c22a4ba7d20c163496a519e815/lxml-6.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e3b666f57a5d81562f38c766c762416b0f6eb58a00590546911514b48412abd", size = 5099288, upload-time = "2026-08-19T04:59:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/21/f120967cc43b54e05512dff0c39726b832c836195d30f41f88733ef36ac8/lxml-6.1.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26ff164c6629e5c4d11c9e55d5ea3d6eed0be2a420eee1f55cbce6e2c23e231a", size = 5036837, upload-time = "2026-08-19T04:59:47.217Z" }, + { url = "https://files.pythonhosted.org/packages/61/ba/8005e9f47598e3ec5c18312c77f94e889580027616678848405c6aeba5de/lxml-6.1.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:962c12b51d0b164f12569af225dea57568477e24a845b96eaccbef6c07e4cc03", size = 5658569, upload-time = "2026-08-19T04:59:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ba/add33b3c7ce51462cf7a4637bcfec2eaa258364d6015b989dd7d1216e6a6/lxml-6.1.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47e367dfe341521426692819803e260d0673899c0ff611f14af978d725e2c999", size = 5246003, upload-time = "2026-08-19T04:59:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/05/b3/a43012748fb861c914c5eac1c1a3bad44282e767499cd02280d4d1edf092/lxml-6.1.2-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:92c2b366028ac01e90399e6d17734ce6e4f4aeddd8ba75fbaf80ea11d6c6d645", size = 5354047, upload-time = "2026-08-19T05:00:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cb/813021d9a445713b8d758b9e5eae2ed392cd598d9f119d9b053b37c2ab93/lxml-6.1.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:7e81fc065ede5d58dd0bf0912025aee1bd04c52c2affd61fdb93226a97ce2fc6", size = 4704382, upload-time = "2026-08-19T05:00:47.067Z" }, + { url = "https://files.pythonhosted.org/packages/17/c9/1155299f4577bebf3c280497534a73e4b8ad8cab3b96074731ad10949d4e/lxml-6.1.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:633ac039cb32366dd5935868e041e385875c017b8cd54ea56aeee3fe29ca5935", size = 5258530, upload-time = "2026-08-19T05:01:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/25/6e/d76e58384b378b877e140e25b9a9835da00035f81ff70cbe943a3749bf27/lxml-6.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f3194777c0d05945ac91d8594be25d2679d1d826e01e1fc90bae568ff3a547b", size = 5089919, upload-time = "2026-08-19T05:01:33.602Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b7/898013c0f8891481d0624ab3bd5dd8c8ff827232dfee2a5d1f8bf970a7cc/lxml-6.1.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1133bd969f2bfcc6b0c0cf7cdf5f2631e62b23fa2471ee8bd44f6ab73554ee9a", size = 4741972, upload-time = "2026-08-19T05:01:38.18Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/efb53c4d7b655831c03317a450d9da439b0829c61f34d9d4fe7c863445d6/lxml-6.1.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1edca8f4a92b94e873093df959f141d388f2141fcad0c47598442fb4730ef57a", size = 5683241, upload-time = "2026-08-19T05:02:00.731Z" }, + { url = "https://files.pythonhosted.org/packages/da/0a/0ff36a584cbba14a71326ee8a5300694400f0b97927d1f90a87d95b17d4a/lxml-6.1.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:8512b3775d68994dd1d6d533161e0a214f2ad9c634659d34a99c98e86c6c3d68", size = 5245892, upload-time = "2026-08-19T05:02:06.108Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9e/303717a1aa56d4bd775c91936717d3c9e8d999a8e8b68b00979c4c1f93d0/lxml-6.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5005c0c9e4d749a76a2ff8bd5918a8bb248df8e08e73a55654b9f79c9cd1e2b", size = 5269528, upload-time = "2026-08-19T05:02:09.883Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1b/cfda9307388d496e7eeb7493d9455896b8137ed95f51f3d6ae6ddcc14a47/lxml-6.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:604f4778632588d7c000e7e19430639dc12fca58b5b6e99edffba7631725ef0e", size = 4979444, upload-time = "2026-08-19T05:02:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/e5/71/f732c8919c45b7f29acf443288c6e90036877a67bfeeb1acceb0fffa011b/lxml-6.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a096d6a5f96b776a5b020cb45c17c545effd2a3b6639e6fa97bc95537600923", size = 5115887, upload-time = "2026-08-19T05:02:23.62Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/121d52b944f41e33ea86c62875f902d24982842dc7231ab154ac5a6c6593/lxml-6.1.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6454d184d556eaf4cb3d6f69e405d21602d6fdcf08b8d57796824275986c6595", size = 5032418, upload-time = "2026-08-19T05:02:26.114Z" }, + { url = "https://files.pythonhosted.org/packages/70/19/cadb73c7fe48c7563dc8ab62ea53d5b920c8911bfb808507a6daa82e78d2/lxml-6.1.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b68f2548259bb04e0b3d5df0c397abe8b0080f5e1ffe4019fb7a8bf01a9339e", size = 5603304, upload-time = "2026-08-19T05:02:28.694Z" }, + { url = "https://files.pythonhosted.org/packages/13/32/9de126a14d5a5db8c371c5ec869178417db226707b62a47273a95ae6df7f/lxml-6.1.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c9cc4b6532abe154dbdebb42aaba8d52c852919591e45067f5b7d46a0405e88", size = 5228938, upload-time = "2026-08-19T05:02:30.99Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/22dd9e843629ed04652591fb220eb2bf2394d97be3be377d60d8083405d7/lxml-6.1.2-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:57188e441ab24f906bd5a5c14eb55363ab51aa6c0de549f3dd320043721cc118", size = 5317790, upload-time = "2026-08-19T05:02:33.301Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/b12a1dc121f81c280635c721c7bcaa341441fcbe37397f60b8915048aece/lxml-6.1.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d0bfd719c254bbe60ea022cff0e6ffb799a6fa7d4d72852cebe0257957b32d68", size = 4646468, upload-time = "2026-08-19T05:02:35.504Z" }, + { url = "https://files.pythonhosted.org/packages/57/41/fd87a41edc531e7969c25ab1d6b52b5b041eb108b88f6394d6afb4374396/lxml-6.1.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:be6f87cd224254a8f81324e34cc655508b83f1d70458a1a39857ad2aa9925852", size = 5240607, upload-time = "2026-08-19T05:02:37.805Z" }, + { url = "https://files.pythonhosted.org/packages/6e/30/713ba813b6e6673c6dc34733746516017efcd17949b767b154cc50bccf20/lxml-6.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:074a88f70a7360a4a0c5be5d898062cd26f898c25b459efb1bdd43ae700c5a1a", size = 5086495, upload-time = "2026-08-19T05:02:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/33/f8/6532ce0fecd9c326d06b08274ee075cc28dbc9f5e9285355db8504689114/lxml-6.1.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:9031f5f01452681abf39fdd65f84a70cb01a7572a1bbf570042e826b1232d07b", size = 4758801, upload-time = "2026-08-19T05:02:45.434Z" }, + { url = "https://files.pythonhosted.org/packages/74/b6/5a1f7833ebaa0dd33c28f6f9755ec6ff3891bf63f097634b44e6da1bb65e/lxml-6.1.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:cfeac14425fc7a6fca7864b774d4ee63547926158f4a18c67d77b2c9a948acf1", size = 5626977, upload-time = "2026-08-19T05:02:48.092Z" }, + { url = "https://files.pythonhosted.org/packages/e6/20/6ae0fc1b45e20877cdcfb1168ceeaf9abb0fba5ed36bd639a260e7b2101e/lxml-6.1.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8ec111ff8067325f85c08aa9c2b26179ec0537bb89c003fde31127139f85f82d", size = 5235036, upload-time = "2026-08-19T05:02:50.726Z" }, + { url = "https://files.pythonhosted.org/packages/47/b4/2bc7b37fbb990ccfb7d30393660741592177224a94e07d842c8da70638e8/lxml-6.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:48e912f37c99a297175ba955f55a47c0e1c834b506ef162e52a6e4fe276e6e45", size = 5252270, upload-time = "2026-08-19T05:02:53.454Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bf/25e26b089510940a0777ab334357874569255e50930224c8159cd649e754/lxml-6.1.2-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:827438bf6c8292d22a409bb7990d7cffce410f33e7664e46ca74d2ecc26975ef", size = 5037527, upload-time = "2026-08-19T04:58:46.224Z" }, + { url = "https://files.pythonhosted.org/packages/65/6d/aed3a58a3d662f7367a537fabe8c549f1446dbd043719e0ae8cd53f47819/lxml-6.1.2-cp315-cp315-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c470d192e27f97842a068cf12a1c1296b20ca716c56a9249715c6654bc192d19", size = 5661918, upload-time = "2026-08-19T04:59:02.534Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ca/706d32b6957c0c2e005a9833e8fc528449196b38d5cfcf9e0fd86a96fb00/lxml-6.1.2-cp315-cp315-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef0b8ba6e13597f681b2b4924ca9c4e8c88420bf0e21d9a9006c757f2fc39d1f", size = 5249359, upload-time = "2026-08-19T05:04:01.956Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e9/445ff43f56fcffa06f6f3a7189920c216f3eacef68ef834d4111cdbd86ba/lxml-6.1.2-cp315-cp315-manylinux_2_31_armv7l.whl", hash = "sha256:65c32ddc5d0750129c7b119fb57d48192b76d334c21e6b690d19dfb06b34af79", size = 4704548, upload-time = "2026-08-19T05:04:04.57Z" }, + { url = "https://files.pythonhosted.org/packages/69/78/20b8b7e79a1b1d9cd4465c332d62962858562b446692f16a27068fa54b85/lxml-6.1.2-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0aa07065497f191ad26c4b587ce5dbb5a7105285a3789aafd0661750e8bac537", size = 5261170, upload-time = "2026-08-19T05:04:07.336Z" }, + { url = "https://files.pythonhosted.org/packages/54/ca/84a0e1148bf511e12e0d99732a4e136a3bf1b91622f0a1b197796e2ff984/lxml-6.1.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cde6b8db7d2e5135129eb5e74b7b44dd2053aa767cd5023541fccedddc262453", size = 5090576, upload-time = "2026-08-19T05:04:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f8/1ef6fc7070bed8753315f2e4ea66bc0d37620e1444d014db7f0267b8faaf/lxml-6.1.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:b28842b30c4bc2e6afe137d98a5d2071a62589471e76d053bea55b0e53298af9", size = 4744614, upload-time = "2026-08-19T05:04:12.717Z" }, + { url = "https://files.pythonhosted.org/packages/87/f6/3a4824cd1c1b81d996d2d75bbd176ba13fbe9b5d89489290d93ff9558486/lxml-6.1.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:11f529062255209a421ae4de5b1bb36b2f0a2e1a700745e675a4bf4084d13c00", size = 5685792, upload-time = "2026-08-19T05:04:15.367Z" }, + { url = "https://files.pythonhosted.org/packages/64/9a/f133bf16a67149e00ca5d8a8f1ae662c30a86c303aa242693b67f8e19856/lxml-6.1.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:f8b89b3be75a37509602b03f9cfa1a28298d4eed4625748148307aeb907901b7", size = 5248972, upload-time = "2026-08-19T05:04:18.491Z" }, + { url = "https://files.pythonhosted.org/packages/50/63/273e7e8a73a5d183d8552dfdaa131dfda0292ddab7bcddc5a66a0ae525d8/lxml-6.1.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:1a2331da06dd55a8184985306eb2afd72d708283ce7e85d67bba77317b785060", size = 5271809, upload-time = "2026-08-19T05:04:21.448Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ee/502f7c93507f57eb496744a64da8f4ca86855cf88e48d14584342f1bfd92/lxml-6.1.2-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47c92dc5167de16e27ace8332454f12ba172dcab04f7a78a9eae14e2e41b6a41", size = 5033396, upload-time = "2026-08-19T05:04:30.054Z" }, + { url = "https://files.pythonhosted.org/packages/bf/72/c4cbbe72f951650f2afe43a70e51687e111d82b9bec46e3310ea76419d46/lxml-6.1.2-cp315-cp315t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:40366c23a938008a3bedfcfd80709b3a857c188b4d710b083e978ef5d2c1c715", size = 5615298, upload-time = "2026-08-19T05:04:32.752Z" }, + { url = "https://files.pythonhosted.org/packages/14/83/a3df966d6d7b6513e9dfb6fbfb041c0619642170359c1b36ab20a83e59eb/lxml-6.1.2-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c4c6dc1b2485aaa4adfb6ed754f90dddcb2b96a66bbebc9e1ac242b5ce5e818", size = 5236282, upload-time = "2026-08-19T05:04:35.762Z" }, + { url = "https://files.pythonhosted.org/packages/4f/85/8692ec8173c9f8d295735b9bf410d202317e7b3ed11141e80a30f421f409/lxml-6.1.2-cp315-cp315t-manylinux_2_31_armv7l.whl", hash = "sha256:3a698fad6f122a9b3e2dc2fb598c1de7329c74a67c7a334c9109a440de2508e5", size = 4650647, upload-time = "2026-08-19T05:04:38.396Z" }, + { url = "https://files.pythonhosted.org/packages/11/e7/dbe3cece28a5bf82997a091d9dbb0fc49e725a5fa86550897ee2cf6412e6/lxml-6.1.2-cp315-cp315t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14879fa5eb2b793c040bbfcb62011aa3015c65d6c9875e063ea98ce2029d51fb", size = 5243387, upload-time = "2026-08-19T05:04:41.247Z" }, + { url = "https://files.pythonhosted.org/packages/99/a9/81a2d27640db0d27200b2f32339a54e74c36d58feb5ad528b87d52a59ecc/lxml-6.1.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b631174cd2e4d9f8a94ef17f911c6ded10ede93b5e7860dee7bbf85961d321e9", size = 5092624, upload-time = "2026-08-19T05:04:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f4/0b0304c70c087f618d95b0306738b070bd556afd09c2c92589b78dbe5eb0/lxml-6.1.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:ceafa5e0536c62a5cd9f65327fa0b57d6f0b0e3435daf2c98a78d0dde7ecbae1", size = 4758742, upload-time = "2026-08-19T05:04:46.615Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/f9fc45f1d01b632b673e11880e75292dff9953db9f426d1a38201b8eb5f5/lxml-6.1.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:7c482e87cc86bed78a50462560675bc2c348ef72c47596f9b933346d5a8e920e", size = 5649540, upload-time = "2026-08-19T05:04:49.777Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0b/d65e0458c2bcce0df68d5cc29ad0006e76446f02d9e50caf188fd1fb8bae/lxml-6.1.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c0d2dde8a50520efc51644587f0fc4810e3af7d3e029d7af0be93bf39e2b5c", size = 5234869, upload-time = "2026-08-19T05:04:52.972Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/1fee828238badd3bfe9544f5cc9ce6ded421ef38e9634030445dedd78b36/lxml-6.1.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dd7ea3fa47154b9fff90591b961e41b3718bd7fcd5bc2d9bb47e9845c8ace088", size = 5259992, upload-time = "2026-08-19T05:04:56.028Z" }, ] [[package]] @@ -2136,11 +2187,11 @@ wheels = [ [[package]] name = "narwhals" -version = "2.24.0" +version = "2.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/7b/6248dada39781db1ab3ebf08943080df0796098515a87f6f8696d14ec744/narwhals-2.25.0.tar.gz", hash = "sha256:62c036c810662bf7820b7737077176313bc59350eeeefb808510f388c743e4b2", size = 677076, upload-time = "2026-08-20T18:10:15.454Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, + { url = "https://files.pythonhosted.org/packages/eb/dc/55481808fd70ef1567cf13540ffd4702af3f74b112e35427564b03f79c2d/narwhals-2.25.0-py3-none-any.whl", hash = "sha256:1f0f403e8c7e4463cde9bfe78b12fdd809e3ae3dda6d9b2f802934fb9c7a6a8f", size = 467373, upload-time = "2026-08-20T18:10:13.834Z" }, ] [[package]] @@ -2185,7 +2236,7 @@ wheels = [ [[package]] name = "nbformat" -version = "5.11.0" +version = "5.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fastjsonschema" }, @@ -2193,9 +2244,9 @@ dependencies = [ { name = "jupyter-core" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/80f407a9525bc5bd9865e5c37db3b78867fa43217f8aac5eab22b5f028b3/nbformat-5.11.0.tar.gz", hash = "sha256:7dbaed4a69cae28c2b4d44ab7430a6af4544fb89455023f6f21550be757b60c8", size = 151822, upload-time = "2026-08-06T12:29:55.597Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775, upload-time = "2026-08-17T08:10:51.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/4a/0eece9dad5e73230ca972f7bc29456ce7d74772f123d401fcf67379008f7/nbformat-5.11.0-py3-none-any.whl", hash = "sha256:f70a17f591a9ccd1c601d5e61a4b20972703926df0ba42458ce14bf575766bb6", size = 79820, upload-time = "2026-08-06T12:29:54.178Z" }, + { url = "https://files.pythonhosted.org/packages/1a/69/ee613f74085ca7103f79cd08d579c4f3177d1d26e5d3d9528d2d6536a707/nbformat-5.11.1-py3-none-any.whl", hash = "sha256:cc6698fa75f4fab8755ead786317815f13a6fee3b53311c0abb1a8b51d52f7ec", size = 79849, upload-time = "2026-08-17T08:10:50.18Z" }, ] [[package]] @@ -2223,28 +2274,28 @@ source = { git = "https://github.com/esheldon/ngmix?tag=v2.4.1#0b5c835ec020ed925 [[package]] name = "nh3" -version = "0.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/1b/ef84624f14954d270f74060a19fc550dd4f06656399447569afb584d8c06/nh3-0.3.6.tar.gz", hash = "sha256:f3736c9dd3d1856f80cd031715b84ca75cda2bbb1ac802c3da26bfce590838d7", size = 24684, upload-time = "2026-06-22T00:47:02.008Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/e1/e96e7864a7a53bd6b6fab7e9632467382a2a2c1f3fed951918ad131542fb/nh3-0.3.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e196fa70c2ff2eb4de7d3df3108f8f358c1d69dff20d45b11f20a5aa227ffb6d", size = 792570, upload-time = "2026-06-22T00:46:22.179Z" }, - { url = "https://files.pythonhosted.org/packages/59/62/5b6108bedaef2b2637fed04c87bdbcb5967b9961758b41f0e466ef22a022/nh3-0.3.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:34d2b0d934156b87ee114f599a3ba9b8b9e17b5d79652ba3a13fa50903de965e", size = 842243, upload-time = "2026-06-22T00:46:23.801Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4a/526f199626bfcb496bc01a268051b44737962005553b158e985ed7e64865/nh3-0.3.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2f14b7ae1fca99c4a66c981aac3974e7fbc1ca30a12673d223ae1df76680917", size = 1001468, upload-time = "2026-06-22T00:46:25.481Z" }, - { url = "https://files.pythonhosted.org/packages/49/09/0d8e3101636d9ad88cdefb2914e764cb8e876ebdbb4286bfc251277d9c67/nh3-0.3.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:889932a97fb4abb6f95fef1914c0d269ebfb60011e67121c1163059b9449dbb4", size = 1082933, upload-time = "2026-06-22T00:46:27.15Z" }, - { url = "https://files.pythonhosted.org/packages/09/a1/ea83abe738a3fbaa203dfdb836ca7cbab0e7e9609faaee4fe1d4652599c0/nh3-0.3.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edb2b4a1a27523e6cc7c417f8d21ce3d005243548b93e56b762b66b0c7f589f9", size = 1043120, upload-time = "2026-06-22T00:46:28.89Z" }, - { url = "https://files.pythonhosted.org/packages/66/69/0654482b8635012fbae67826bd6c381abb05d841ac7388b9b4666300fdad/nh3-0.3.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43bc1ed3fa0716295fabee29ba42b2667e4a51d140b0a68e092170a765474fa6", size = 1023824, upload-time = "2026-06-22T00:46:30.453Z" }, - { url = "https://files.pythonhosted.org/packages/41/21/e1084ab18eb589506335c7c7576f2d4643e9a0c0e33983ef0e549a256b96/nh3-0.3.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1b160831c9cdb06a6c79c2f9cdb11386602938f9af260d1c457a85add4f6f69", size = 838002, upload-time = "2026-06-22T00:46:38.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/94/f48d08e6f72a406300fa11d8acd929fea1a80d4bf750fa292cb10785f126/nh3-0.3.6-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d14bf7982e7a77c0c775634c29c07ce08b38a046df73e1c1f139b3e82f18a38e", size = 823045, upload-time = "2026-06-22T00:46:39.495Z" }, - { url = "https://files.pythonhosted.org/packages/25/bb/431615ba1d1d3eb63cde0f974f2114edf863a8a3f6049a12fed23fc241d3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:44673b27010051ab5a5e438a86ec31bbda61d4a77d7e900af6b7be3037c1abae", size = 1093171, upload-time = "2026-06-22T00:46:41.21Z" }, - { url = "https://files.pythonhosted.org/packages/0e/24/a0d80182a18919665fefd19c1c06f1d1df1c9a6455d0252de40c034a0bc3/nh3-0.3.6-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6b7beece07525dc6e6b0fc2f104442de2ba328360ad00e50cbe2e1fd620447d", size = 1049217, upload-time = "2026-06-22T00:46:42.804Z" }, - { url = "https://files.pythonhosted.org/packages/0a/13/6f1e302ca674ac74362e150848ad56a1be5145391204f74facdb8e94df12/nh3-0.3.6-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:455469a29951edc92bc48b47ac2281c3f2609e6c4f6a047056449f8c2c23facf", size = 917372, upload-time = "2026-06-22T00:46:44.495Z" }, - { url = "https://files.pythonhosted.org/packages/5b/67/314f6151bad77a93d751978a344033e1fc890822f05f0416079338e34231/nh3-0.3.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905f877dc66dd7aea4a76e54bcb26acb5ff8216f720c0017ccf63e0e6035698e", size = 806699, upload-time = "2026-06-22T00:46:45.99Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a6/bfaa00046e58603507dcfc266c4778e3ab7adf68a5dedd73b6274b8d9314/nh3-0.3.6-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:25c733bee928530556b1db0ea46c52cf5aa686146e38e60a6fc7cb801ef91cec", size = 835165, upload-time = "2026-06-22T00:46:47.617Z" }, - { url = "https://files.pythonhosted.org/packages/30/a8/fb2c38845efb703a9173bffdfc745fc64d2b0e55cfc73a3647d2f028250c/nh3-0.3.6-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f90d9a0cfdbee218994fdaaeeb5a0fde62d08f35e4eef0378ec1e2200172fd0", size = 858282, upload-time = "2026-06-22T00:46:49.276Z" }, - { url = "https://files.pythonhosted.org/packages/68/17/06e72a18ee9b572914447338237ca7eb164c0df901f141bc10d1282247a2/nh3-0.3.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:82ca5bf427ad1b216b65ede1a2e2d87dc49bec417ceba0f297213107d3cd9d78", size = 1014328, upload-time = "2026-06-22T00:46:51.026Z" }, - { url = "https://files.pythonhosted.org/packages/11/f9/3966c61455668c08853bf5e33b4bed93c421f3194ce4de896dc248d6f6ce/nh3-0.3.6-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5ed5fe84aee7f39db95c214a7421bf0499fbf500fec6d86a4e29bfc37971438", size = 1098207, upload-time = "2026-06-22T00:46:52.674Z" }, - { url = "https://files.pythonhosted.org/packages/19/d3/479cb4ae440424825735d60525b53e3c77fd60fd6e6afc0e984f00eb0178/nh3-0.3.6-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:082675ff87b9385ec430ffe6d5847ba7456cc39b73720cd4add472f9f4cffd56", size = 1056961, upload-time = "2026-06-22T00:46:54.335Z" }, - { url = "https://files.pythonhosted.org/packages/17/0c/6cdb5ee1e127be50dc8391e54bddc1f64e87bf4bfad0c55633320e2e02db/nh3-0.3.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:36d06341bd501240d320f5942481ed5e6846136b666e1ba4faf802b78ebc875f", size = 1033829, upload-time = "2026-06-22T00:46:56.258Z" }, +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/2f/022b27146d52d24b1b353b003359134788ecbcd6fcdf6283adbd57c0fbc8/nh3-0.3.7.tar.gz", hash = "sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848", size = 25662, upload-time = "2026-08-23T14:26:30.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/60/847a21339f095c4d4c655af31fa2d18b174585bcc210709facacc7ce205c/nh3-0.3.7-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b", size = 820463, upload-time = "2026-08-23T14:25:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/1a103e00aaf5e59f2dee4c2709aac609bb2d4bb74fddaf0dcfade11ed87b/nh3-0.3.7-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32", size = 861456, upload-time = "2026-08-23T14:25:58.087Z" }, + { url = "https://files.pythonhosted.org/packages/d8/4a/e9c436089a0c80b928011ead0efd156aa7639a19b6064ef58dcedcab8369/nh3-0.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa", size = 1023930, upload-time = "2026-08-23T14:25:59.465Z" }, + { url = "https://files.pythonhosted.org/packages/04/5c/aa1468e3e281e78d2b3b7d762ccba59f681af355e971dbd255d5903f7b86/nh3-0.3.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac", size = 1102614, upload-time = "2026-08-23T14:26:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/57d186d9d3dd38905dc12dddb3484406cdf6aa0b1ce33639a2d277d4ee1c/nh3-0.3.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102", size = 1059915, upload-time = "2026-08-23T14:26:02.388Z" }, + { url = "https://files.pythonhosted.org/packages/6b/53/097a5ad0b34b15d67a472ef849165a54209fa5fbd3e639801c6fe439ba28/nh3-0.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a", size = 1047402, upload-time = "2026-08-23T14:26:03.897Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/a687e7091928806e514f89fa2666f25ec9bfe0a902fc4402b25e51ce408b/nh3-0.3.7-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479", size = 859615, upload-time = "2026-08-23T14:26:10.606Z" }, + { url = "https://files.pythonhosted.org/packages/85/05/b0e6bef633549a23347d5462aa288fcc42381e7918482062ca3cb456242a/nh3-0.3.7-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506", size = 839872, upload-time = "2026-08-23T14:26:12.037Z" }, + { url = "https://files.pythonhosted.org/packages/17/40/2a0921d45b20828708bcb56887e47dcf8cae13818de5bf9a01308d348712/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086", size = 1091325, upload-time = "2026-08-23T14:26:13.34Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d1/9d70e0e418a48280ec0ddc6c1b08b4b1136ebcc31a1625e57ff5c665fa51/nh3-0.3.7-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563", size = 1042482, upload-time = "2026-08-23T14:26:14.667Z" }, + { url = "https://files.pythonhosted.org/packages/93/a7/02dd159d4e71f98607d8d4249cddb7561e77be1a8e4dec77d76e1b68fc99/nh3-0.3.7-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174", size = 946868, upload-time = "2026-08-23T14:26:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ed/c5510c615dce55b6fcc364aa1838142f938beed64f5e4927490dfcaf4405/nh3-0.3.7-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42", size = 832161, upload-time = "2026-08-23T14:26:17.272Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e3/3212c1a5b5745245d7f18885207bbddb34c56075f34dd682bd539aad55cc/nh3-0.3.7-cp38-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8", size = 849791, upload-time = "2026-08-23T14:26:18.498Z" }, + { url = "https://files.pythonhosted.org/packages/20/64/9e36594efad6c290de4240d02cb2bd80c339a4ab1c4de66e599ffa6d9d81/nh3-0.3.7-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493", size = 875473, upload-time = "2026-08-23T14:26:19.908Z" }, + { url = "https://files.pythonhosted.org/packages/00/0c/1a8985fd43fea5530c0ac890b6f0b423770ee72f111b70b7a77f2dec243a/nh3-0.3.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd", size = 1036463, upload-time = "2026-08-23T14:26:21.536Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5d/891e533b716cf00df76ad0ba6485dcfd14d59a6430a3cc99057c4c04004e/nh3-0.3.7-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac", size = 1116029, upload-time = "2026-08-23T14:26:22.907Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/ae8c0782fce74fb6fcf7234bb3d4017f37ce181b4f9d29369eab21c50a04/nh3-0.3.7-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62", size = 1076589, upload-time = "2026-08-23T14:26:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/26/a4/c3423351e8d864ad756e85e15f0c01433361f14d34e4ed156482c0518f2a/nh3-0.3.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af", size = 1058871, upload-time = "2026-08-23T14:26:25.674Z" }, ] [[package]] @@ -2492,11 +2543,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b18 [[package]] name = "platformdirs" -version = "4.11.3" +version = "4.11.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/06/cf1564dcc2e2261c8c8c6c05628dc8b418943bdae2a4e58640ceb2f770fa/platformdirs-4.11.5.tar.gz", hash = "sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173", size = 34823, upload-time = "2026-08-27T21:36:37.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" }, + { url = "https://files.pythonhosted.org/packages/c7/12/6f3fcd5067a9cbf4f8664b32957973498da8b083455203c8d9cab83a725c/platformdirs-4.11.5-py3-none-any.whl", hash = "sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b", size = 23900, upload-time = "2026-08-27T21:36:36.227Z" }, ] [[package]] @@ -2656,7 +2707,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -2664,62 +2715,62 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, - { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, - { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, - { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, - { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, - { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, - { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, - { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, - { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, - { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, - { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, - { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, - { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, - { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, - { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, - { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, - { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, - { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, - { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, - { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, - { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, - { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, - { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, - { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, - { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, - { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, - { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, ] [[package]] @@ -2771,11 +2822,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.20.0" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] @@ -2932,14 +2983,14 @@ sdist = { url = "https://files.pythonhosted.org/packages/f1/90/29b10f180876a322c [[package]] name = "python-utils" -version = "4.0.0" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/4f/ffd054f649cb9deca624a61d83fd17ce5b6247ba6c41a5881b0c639573d7/python_utils-4.0.0.tar.gz", hash = "sha256:51d84b297d9877205a76e65f970e3275d1aa9c1cf3d508af2c459247eb4fe701", size = 43027, upload-time = "2026-07-02T18:07:54.953Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/71/ec6665d4ce42ee5a59fffd31a4d5164f92da15ccb8c758dba13d2419ea53/python_utils-4.0.1.tar.gz", hash = "sha256:4e8e8ecaba3862f843a60c1982c99cda23b522f417006a807996a876c18beb8d", size = 43765, upload-time = "2026-08-30T19:26:04.591Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/89/daba77b0167e3e03ead14cf0caa02193b6b9d402d03db93651ca9c9f1815/python_utils-4.0.0-py3-none-any.whl", hash = "sha256:96b969a878bbc147e69db60aac7c618ff97f7e5e03e9d78ade85d764af71faa5", size = 39928, upload-time = "2026-07-02T18:07:53.673Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/df2b7b53f759af128123afc73ce7094a29286ef4293dfeb2341f8469018e/python_utils-4.0.1-py3-none-any.whl", hash = "sha256:6017bb56498a58656ecf48e66fb1f1632c7b33f9f2968548a021563e85be1aed", size = 39920, upload-time = "2026-08-30T19:26:03.101Z" }, ] [[package]] @@ -3025,31 +3076,32 @@ wheels = [ [[package]] name = "pyzmq" -version = "27.1.0" +version = "27.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, - { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, - { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, - { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, - { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/ef/c08b91248bb90a9efa81fa00ba81b69c157c74d0c5efbb2c319d91babb62/pyzmq-27.2.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:00e73942ef12cecbc7951c4a9104bb8ffaed742abb13af2da6833d90dd368cef", size = 973915, upload-time = "2026-08-20T19:06:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/a3a3a86c2b00fadb92ece1ca4f8f028d62b2ce9ac3526097239ab2d6fba9/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f8079d0521fe94bbb401fe9407578b28f3701627c8be2c9f7e0c5b77dcb0109", size = 697722, upload-time = "2026-08-20T19:06:43.325Z" }, + { url = "https://files.pythonhosted.org/packages/62/2c/d5828306f795e8d34676d266823b74e2101e0ad3760d12083de3e02abbb2/pyzmq-27.2.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dea74fd65f1fc5f7fe167916a473ebe6ed6174e5e5d9de11ea6583661be6cf43", size = 872258, upload-time = "2026-08-20T19:06:44.627Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/51253b78fd8739293e283407eeecb14215c02c71b6519af21f6eed8e69cd/pyzmq-27.2.0-cp312-abi3-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dcc99ca132b667a4ed750afd42db4ea73288f18425a9b2e3c0af095665c491f5", size = 739591, upload-time = "2026-08-20T19:06:46.214Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3e/142c85b67a4c9678629b0cf6d5125b29663d75be69bfaa57a3cac344d780/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b8d5f66e4a8246cf77f7b8f7902af64f00553368fa0373c89d99b78f0ad79394", size = 1689031, upload-time = "2026-08-20T19:06:47.612Z" }, + { url = "https://files.pythonhosted.org/packages/0e/ee/0776fb0f98ed1eb74d77240087fef0ab045b6ad15cb09555c6c5134c98ad/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:d1526b42a2e725b84ed226f37becedc250c6347594e5ed304e4e9aff68c9aec3", size = 2059547, upload-time = "2026-08-20T19:06:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/ec77f691a4aebe29ab6329f996fb0e0270c876a3016086e3ca6ef733bcae/pyzmq-27.2.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f707bcf2c1d007d14d70531d4dd7b41060881c73efa845580bf6faaf9ea24d42", size = 1910457, upload-time = "2026-08-20T19:06:50.783Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b4/def7a478458da78665840564161772e7e938600c32a89f28e8b221b54d2d/pyzmq-27.2.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1ecbdd131b9669f62d3a45afee5527c7ae9f141e4301267f21714c90bd21725f", size = 975868, upload-time = "2026-08-20T19:07:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/38/d5/e3e85f7fea37153097aaff49db9e33093909cc2a7b22c1ac4ebe546600fc/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3146385b94a760236c5eceff468a66a296a716ca98a2e0f9217b1518118466b1", size = 706054, upload-time = "2026-08-20T19:07:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/3b7d9449b223183222bf517245e1e53d5f1ab8c10be8b45f6a301b2f994a/pyzmq-27.2.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9846e881620dd62566ca76a53e384c3f37490faf4b9240aebc7498810dfca853", size = 878984, upload-time = "2026-08-20T19:07:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/be/a5/8b49dbd494f6dcfda69dc4cade322a4b02706ef4e3d30cc366d4e369899f/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d9527e3dbaef1edaeeb2446fa7379446814a43ade8adc7c4a5ebe69437815ddd", size = 1697489, upload-time = "2026-08-20T19:07:08.945Z" }, + { url = "https://files.pythonhosted.org/packages/da/5a/4bb8280901130c26ea25f0cbb4a6d39d94250860c6b3dbd912f1cf48fca7/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:56b48fa9d478a3af7254f397697a62f5ad3e1bb677e200b2701f0c290d97e5af", size = 2064236, upload-time = "2026-08-20T19:07:10.384Z" }, + { url = "https://files.pythonhosted.org/packages/de/38/f433af66922554adb2b5f79e897018c8e19a90b9eaeb49c4814f8355ebe4/pyzmq-27.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bf0b6e4ce1bb089751c504c5493d6b0557eabd02dd21b76e9086cf964234b103", size = 1917424, upload-time = "2026-08-20T19:07:11.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/46/fb56f3f37a6a0937b0e1d2885e808b5eedc171320bac85573cfae78fa9bc/pyzmq-27.2.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e0fa0bc6b1a184aee59b32efcd1b7f0e6d5b8f9387799e4c16a4cb66a86747d6", size = 976118, upload-time = "2026-08-20T19:07:22.577Z" }, + { url = "https://files.pythonhosted.org/packages/21/82/a2c9bfd7c4d34eea1278493cd041bc000d41acb4463c89ceaad29dc813b6/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4bd6743e8bf854c3bfce892dd6578a514aabf128e37a4b2eafcf01856f7e44", size = 705968, upload-time = "2026-08-20T19:07:24.019Z" }, + { url = "https://files.pythonhosted.org/packages/d6/12/b906b269116b6591dc15c0acc5d04c043957c8a531d336999731f4b1d899/pyzmq-27.2.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95369ed6626afcfe2ac89832fb1b917c077fbeb905fbbe5d918349ce0222b89b", size = 879011, upload-time = "2026-08-20T19:07:25.428Z" }, + { url = "https://files.pythonhosted.org/packages/12/13/f96359534bfb77651c15f1fbfc4bfdd7ec3489d23f434706d39598dd0dcd/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:40124779c3a56ad5d91902df1ff89159cb414b6c1a0ee697abcc66cf5e6db62d", size = 1697496, upload-time = "2026-08-20T19:07:26.821Z" }, + { url = "https://files.pythonhosted.org/packages/21/b4/2c007ae5f2fe5eca86cbfbc874ed86b5135f2f7812615dfd78606d3c93f6/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:ec8a318dfc27c7d946651b3d9e8025d5734f30c168a822195601827207bac09b", size = 2064347, upload-time = "2026-08-20T19:07:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/9b/88/767af3a6630c15215f3a66700ec79598a375edd1fdc9d75a3ad522178c01/pyzmq-27.2.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:88c0fac061bac269076edeb3a209acefc96cd6167c239daf1c2b404ac48d7012", size = 1917360, upload-time = "2026-08-20T19:07:29.693Z" }, ] [[package]] @@ -3066,16 +3118,16 @@ wheels = [ [[package]] name = "readme-renderer" -version = "45.0" +version = "46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docutils" }, { name = "nh3" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/02/51/d3a6ea424652c60f05600d8c2e01a55c913755e7cdad64afabbd1aa16f44/readme_renderer-45.0.tar.gz", hash = "sha256:030a8fac74904f8fba11ad1bb6964e3f76e896dc7e5e71f16af190c9056696d1", size = 36172, upload-time = "2026-06-09T21:05:17.37Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382, upload-time = "2026-08-28T15:18:32.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/1b/295bf2fa3e740131778065e5ffa2c481f0e7210182d408e9a2c244ff5b0c/readme_renderer-45.0-py3-none-any.whl", hash = "sha256:3385ed220117104a2bceb4a9dac8c5fdf6d1f96890d7ea2a9c7174fd5c84091f", size = 14134, upload-time = "2026-06-09T21:05:15.85Z" }, + { url = "https://files.pythonhosted.org/packages/4b/72/ac5ca81fe9121fcaa9d828d21017cba00a16a98e4ea5fb60c878f93dda4f/readme_renderer-46.0-py3-none-any.whl", hash = "sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560", size = 14239, upload-time = "2026-08-28T15:18:31.132Z" }, ] [[package]] @@ -3275,22 +3327,22 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.3" +version = "0.16.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/85/c8e12473c93018f92d19dd988a294202e1c27426c47ec4de53ffb847b8d8/ruff-0.16.5.tar.gz", hash = "sha256:1b88500f9ffbcab3dedb0082c9f9492e91ec3d618aac1236a3e0189938f7040b", size = 4912003, upload-time = "2026-08-27T16:34:18.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, - { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, - { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, - { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, - { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, - { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, - { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, - { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, - { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b6/77c90a970fe2dae17a723acbd011043ea97c98d7deacccefdc4ba74ec512/ruff-0.16.5-py3-none-linux_armv6l.whl", hash = "sha256:12e5f673e774c35fbb62f288809c7653b73445f8ecec6b6063fd6ea3521aa14b", size = 10011941, upload-time = "2026-08-27T16:33:41.287Z" }, + { url = "https://files.pythonhosted.org/packages/43/45/a684caacdedaca180f52bacccc40bf0789d2c5a7c75f25324853e9eaedb5/ruff-0.16.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b719b0a1f4d59710d283ab2965f621684a108a9e41da622e3b23f0326cd0025", size = 9964129, upload-time = "2026-08-27T16:33:48.352Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/5d2bcdaca6b5b93d1b4dfc166cd2aebf7680143a1b38a28759df13a94d31/ruff-0.16.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2298f2780ed1be0c5cb1361e32ab7b1467f3cce7dabe101d2210a314f2fe42e9", size = 9821518, upload-time = "2026-08-27T16:33:50.57Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ff/011cce29accf9257d5974145b733fc653a37985ed6825413a3987cefbfe0/ruff-0.16.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:258f29035a2dd021e7861e631b227a5b3f14e50c1184c9a6a122c5f4576154d7", size = 10534835, upload-time = "2026-08-27T16:33:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/d7/5a/f0cf109bada9bba0e96c90c21c9f9251803f57225c32d293327a03c710d6/ruff-0.16.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9a4f0432966834019c74d1b7e5c51224305d7713f3d7faf3e7451f1a3be3cde", size = 11252550, upload-time = "2026-08-27T16:33:54.521Z" }, + { url = "https://files.pythonhosted.org/packages/63/4d/1d481aaea2046c6a7ed7c291f9004c669cce3c087b6b376ed5b08271e3fe/ruff-0.16.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5eb3a8c3d0ade9cea42b591fd530368e8798380e30e0a308b85a5cf718f09ea", size = 10777949, upload-time = "2026-08-27T16:33:56.88Z" }, + { url = "https://files.pythonhosted.org/packages/ee/34/ee245ca55f64443233034b3d02b03236b19242004281247c079390b7facd/ruff-0.16.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0f69e191a13a3c9816f63163c88790cb12cd157bbbb384e9c44745702ab105", size = 10311656, upload-time = "2026-08-27T16:33:59.12Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4d/c33a333e341c0a2b96c715b52d89a606f5a34cd4ac493cd9b8d0187186b8/ruff-0.16.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0eeab41fbea2c42f98dfb9822cdccda9d24ba38d49f6dc945b5c236d48f0ef29", size = 10532125, upload-time = "2026-08-27T16:34:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/a64cef78b40192497bb98a27a8aa8f2c98ee9ee15bc97f7712d94ef32937/ruff-0.16.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f0768e9df4300713fff30733c87575f68b6f1d8de41184e505b7fdd9c0c95eaf", size = 10097648, upload-time = "2026-08-27T16:34:03.16Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4e/4cdc9ed3c3e109d2f71e62572a37457298d7bc7501ec3138babb7ed32bbd/ruff-0.16.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:95cc70cdc7aa80c338de356279d2adbeb2de0f520b9ecd8aba75b94e95e02f91", size = 9829344, upload-time = "2026-08-27T16:34:05.134Z" }, + { url = "https://files.pythonhosted.org/packages/39/4a/31ed35ce31729955fc583ee0d176d6e784c1290cb0b0a75cb2134c1ab72a/ruff-0.16.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d185c8398ded1bfd91c0c2cb258346307571eccc473a8490af8c3977399c384a", size = 10277117, upload-time = "2026-08-27T16:34:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a0/60356d86687b4b666d593df213f4dc3041750d024cb7bf2cfa81cfd65c2e/ruff-0.16.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fb8e3a3c4c6a784150a7ced53b015f4b253fc2bf97a610886419ead64b4756ef", size = 10711653, upload-time = "2026-08-27T16:34:09.712Z" }, ] [[package]] @@ -3946,24 +3998,24 @@ wheels = [ [[package]] name = "swig" -version = "4.4.1" +version = "4.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/99/0556919f96b1c291a7ff83793adb328e2a1a5e6cfb379d5acc637cdec959/swig-4.4.1.tar.gz", hash = "sha256:db9a625653f6454530a6c6553d17cf576d7aa2d253c76dc8cdd5979f0cb15012", size = 25956, upload-time = "2025-12-27T03:25:49.609Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/4a/0e273ba5c943f8bd1ec53671146f5062062199297e8dbc0d1f11977555a6/swig-4.5.0.tar.gz", hash = "sha256:eb6594f05a1f87783f981e92501c604ac08cc23bb20fa4e6bd5d3a283819f8a5", size = 25844, upload-time = "2026-08-21T18:28:45.676Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/4d/860e5475fe38b9c7dc36f61d0c370a62b6cc725d2bd11ada1d22a60ff5f7/swig-4.4.1-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:520fde8805b4775ef3814576929b8c44d666220b06d8da625f292192f9e74fe8", size = 1917511, upload-time = "2025-12-27T03:25:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/84/e1/5cd4de23ff2b1d225279f7c7826191b1dbdb77fc858fe12ee7be2562ddb7/swig-4.4.1-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d8cca45a0e8d1e045a1490ea2d05bc1c49e2c266dbbe5954048e2a93d125ee07", size = 2066284, upload-time = "2025-12-27T03:25:19.624Z" }, - { url = "https://files.pythonhosted.org/packages/96/42/d49992f3c39c90d3d46c431a4afea60fc16bae542cf07f394b298a1b21b5/swig-4.4.1-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:bfe6ce6189fc8b983f65f5ed3db8e1d8be03accb0ec3869f986a8343f014e4d9", size = 1912492, upload-time = "2025-12-27T03:25:21.843Z" }, - { url = "https://files.pythonhosted.org/packages/10/2e/4baa50fcf932f9d58bc8394b211c7daf3b7ec1e53c08d560142f1bc99cb9/swig-4.4.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9c98d7b882b6debe3cbb87e6503858e104162d7ce5ab4cfc23425da2e26e51b5", size = 1979872, upload-time = "2025-12-27T03:25:23.48Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7b/e3a14d053fa18b0d2e14efcc21883816964ddbe52a0c43018e195a99aba2/swig-4.4.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ae3da2bf679a4c942a2c100789395d4d167e7da8286018124e4665f5eff43e31", size = 1895896, upload-time = "2025-12-27T03:25:24.98Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a6/c961a911385a4cc4ae10b4dcbcf5dede0407530e59870ce6e4f7bacce47d/swig-4.4.1-py3-none-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7945f153f7fc93f1ab280f44a3740d6bbfeec3829f954784ba83990fc61a11a8", size = 1912119, upload-time = "2025-12-27T03:25:27.661Z" }, - { url = "https://files.pythonhosted.org/packages/86/4b/a08a2673a2fd3185932db110de67a9f6fdfcece6bf9220e18502bf745f7e/swig-4.4.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a9c74b87151d8247d802bda33f78cab9f27c759f0605b760a8f9c9893b5606b", size = 1885810, upload-time = "2025-12-27T03:25:29.562Z" }, - { url = "https://files.pythonhosted.org/packages/7e/38/efa343b506221a9d43bb1132f08b29fcb07d4113165f598d8a1ea733a1e4/swig-4.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a95ae66aebb8ccce4c50febd496e6927b888d98aba272dd5642fe9312bf61a3b", size = 2950985, upload-time = "2025-12-27T03:25:31.586Z" }, - { url = "https://files.pythonhosted.org/packages/de/0b/5b46e61798982f3cb0ed54fc30a30bad5fa6e3bdd1d9177e439976f7a7e5/swig-4.4.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a8f06227e6472832dad3ae34d5bd88710b7bbb109f2927e14a21e6725146e944", size = 2781782, upload-time = "2025-12-27T03:25:33.534Z" }, - { url = "https://files.pythonhosted.org/packages/17/e6/a15efd9cbf39b1289ee7c1acb8d18824a0c2532b85cdbd93acfdf15ee000/swig-4.4.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:98d61b45215982dd3a71745dfc31599176ed79d286b58bc712c10ed031432fdf", size = 3272993, upload-time = "2025-12-27T03:25:35.862Z" }, - { url = "https://files.pythonhosted.org/packages/d6/fa/516b95f9966c57acf1da9f2eb22e4e63104452d6e28686d57b216171ee7c/swig-4.4.1-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:b6909a8bd68037ea0df061a52c664431b062dbdfb1eb90768719518c120139d1", size = 3189093, upload-time = "2025-12-27T03:25:38.158Z" }, - { url = "https://files.pythonhosted.org/packages/0c/03/88ca92d3d8e9dd765a66985202db23745bd26a368570a8d8db9a218bfe90/swig-4.4.1-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:6a6a6331a5c7759ba5a910aa83aa5d33022b8e691b6cb466635c7fc2238facd2", size = 2967789, upload-time = "2025-12-27T03:25:40.24Z" }, - { url = "https://files.pythonhosted.org/packages/bf/97/5ca3d750f294048c983edfad1f2ad093a83480494fccb2f2e2496304f965/swig-4.4.1-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:2d076358cded172d2f44b3c617923c593dbae664b3fa45051641f028f7f74010", size = 3203253, upload-time = "2025-12-27T03:25:41.953Z" }, - { url = "https://files.pythonhosted.org/packages/97/44/96586955c0f08b0b9b6c8b2b68ecb43a815da94d5362cfb6691ede3b9fdf/swig-4.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:43a0a876b4660f6addb6ae72d14a72324dc1388e1c168ed38bb00fd123048749", size = 3060721, upload-time = "2025-12-27T03:25:44.322Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/9a1ef12920e18f6638be03f6adf2f98b3ee23902c6401c85ffccb8cdbc17/swig-4.5.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:59e35b68f32ada308b3d1e13fd2793df9e56d399fa136c50b24fd5a048a94ec9", size = 1961802, upload-time = "2026-08-21T18:28:17.724Z" }, + { url = "https://files.pythonhosted.org/packages/17/1c/f21fadc56c517b072b2b0652bc767fc0e44da5238fe47c3ee9ece73d34a9/swig-4.5.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f166a61e0f31a61f80eb9e22e00649835b30bb92f703ca009c03f719337da1a4", size = 2118597, upload-time = "2026-08-21T18:28:19.51Z" }, + { url = "https://files.pythonhosted.org/packages/7d/bb/add9f6cf59c9c49f3b4eea9260991eae112bf13a750ed76c9cb7739f5e2d/swig-4.5.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4f70be1618a6da71bd50c97222cc78ba1fc194a5ab3352dd9868f8cea10aecb2", size = 1955866, upload-time = "2026-08-21T18:28:21.001Z" }, + { url = "https://files.pythonhosted.org/packages/ef/bd/104089157d29013049b66a171d2ee74486af72b20d1621b06dd0cc998565/swig-4.5.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0ea50150bf5f4782789dc2016ff3ea4f40d2e37ed1954ec8c0442b947fae244e", size = 2026678, upload-time = "2026-08-21T18:28:22.709Z" }, + { url = "https://files.pythonhosted.org/packages/29/85/f5f7976ea09a4a16eee5b96f7d7c289685b19651c51ce9946128f1315440/swig-4.5.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4821eb0b69869af25547fcd00c8235e274e43515d001fd32cf079d52eb7d1d99", size = 1938693, upload-time = "2026-08-21T18:28:24.406Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ef/6139883f1f9b0f0f7a0dee74192ddbd9a5c8b6cc7cee9b0bc390628eefbe/swig-4.5.0-py3-none-manylinux_2_24_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:22c2f089ee929f3c714b70a657fd96ea395fc49890e7f345d175f9bf15c96a36", size = 1956656, upload-time = "2026-08-21T18:28:26.205Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/7b75796cc856636efc69529075a7653801b23632e6aaefefe4a8ee17d976/swig-4.5.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:05e5ec8b5d4fabb6e3a910cfb43b7ebb04851c8c66117795350b80bdcb878d24", size = 1927664, upload-time = "2026-08-21T18:28:27.932Z" }, + { url = "https://files.pythonhosted.org/packages/17/b8/fbac0cdf6ee2b99892c1fb296a8bad60adc6d2c33a49733609334ac39547/swig-4.5.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b297421b229fe23245a2701598a3efd1ba29f4e809aa2cd07b426d52a37ce75a", size = 2997954, upload-time = "2026-08-21T18:28:29.703Z" }, + { url = "https://files.pythonhosted.org/packages/44/1c/d8c4df84868dbe1a0a73d10fed24c36a5e0155f0ba43a72be3bc975d0908/swig-4.5.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:64f37fbaa975403d026a0bdfa4bea27c8e3861f2ee134b37d705d4ad3dd5b435", size = 2826382, upload-time = "2026-08-21T18:28:31.521Z" }, + { url = "https://files.pythonhosted.org/packages/64/e4/c07a09a4be495c70983939c114ccf30d691cf15e61f8a84a444e12e02a3e/swig-4.5.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:83d1ff7c4422ef1a89ca8f65061be4ef78eb5d02a75fae36043a454461f9c57b", size = 3324095, upload-time = "2026-08-21T18:28:33.373Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/28ffbcdee529994f44ec3e9ba6fe19ba6edb10ef07f82609af468c4b7cf6/swig-4.5.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:edf8a275f9c6b1e0ac6b07d7a9e1e1c835c3d2504f9cec6dc863a959b81ab09a", size = 3241787, upload-time = "2026-08-21T18:28:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/bee4314e01445e920598b8abcaf0fd1477c942b6c8ab167c544891ded065/swig-4.5.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d5d479fa163e7ffce448bd00f658f688c9b5234eda501fc60c8cdc8087355c49", size = 3012896, upload-time = "2026-08-21T18:28:37.021Z" }, + { url = "https://files.pythonhosted.org/packages/07/22/d65fc57e270f58cc44fc7f6d8ea56534afb71e65b67089b09f04987b32c0/swig-4.5.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:a8319ab91625d2e15556eae923f8792ce0e3c11fd375912584f218a4f0e7cf34", size = 3250472, upload-time = "2026-08-21T18:28:38.843Z" }, + { url = "https://files.pythonhosted.org/packages/6b/75/d021463537e8cffd4d3f88228184e7b6aa6641ad97f0fc87f0195ec24150/swig-4.5.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:762638d3f1fa1f27b09f8ef6854ffc0744e0f54ead3b19bfec5d22244bb9872f", size = 3105789, upload-time = "2026-08-21T18:28:40.532Z" }, ] [[package]] @@ -4038,14 +4090,14 @@ wheels = [ [[package]] name = "tifffile" -version = "2026.8.16" +version = "2026.8.23" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/22/5de8a60ec7cd23436a9a5e2c5be8866556796307e0f54e0fbc2bf1981677/tifffile-2026.8.16.tar.gz", hash = "sha256:523a18b8183253e1e594162eed45f157c959d8cff6decde3f2d58f20a5a45389", size = 442370, upload-time = "2026-08-15T16:09:35.736Z" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/07/90078f49e60718d414d5440dc498301f11ff049458e65cf8d27c62a5c9d1/tifffile-2026.8.23.tar.gz", hash = "sha256:bd3c816f166f85c93329a54a0c9a1eccc9968a6a78f91d63b65d7b17675915f2", size = 446179, upload-time = "2026-08-23T18:45:05.976Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/49/819b902ecebb2ae136432981dd25b1600e5b0f3633644ac40ae7c11c71f6/tifffile-2026.8.16-py3-none-any.whl", hash = "sha256:a1263c59be7cba87af3c071542b66ae54ccbcb2768bc15b0ab67680641c7f685", size = 271532, upload-time = "2026-08-15T16:09:34.493Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/05347ae8b1b268c068b3ca38822558038f38c6494604837d7ca3cbcceb9c/tifffile-2026.8.23-py3-none-any.whl", hash = "sha256:a03045afce67d97cb4ef56a1cd47eae6ab8115c7e84cfedeb5cadbfbb6d14fbe", size = 273795, upload-time = "2026-08-23T18:45:04.466Z" }, ] [[package]] @@ -4130,16 +4182,16 @@ wheels = [ [[package]] name = "typer" -version = "0.27.1" +version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, { name = "rich" }, { name = "shellingham" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, + { url = "https://files.pythonhosted.org/packages/dc/bf/205d0004930ede8f542fb58f601526fccf4ae7626075ca1e6c4de5d3d652/typer-0.27.2-py3-none-any.whl", hash = "sha256:b3a5fc4342d5fc8fda8fc3010b1cf117e9249aab7fae800c2eff62fd3842d97d", size = 123130, upload-time = "2026-08-28T10:26:53.752Z" }, ] [[package]] @@ -4206,11 +4258,11 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.8.2" +version = "0.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" }, ] [[package]] @@ -4233,50 +4285,56 @@ wheels = [ [[package]] name = "websocket-client" -version = "1.9.0" +version = "1.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/1d/ae61bca2a99d0735311b7b11b81931279b4a45c74a45034d31b39ad565d1/websocket_client-1.9.1.tar.gz", hash = "sha256:2e0916d2e28ccc82702f2f70aa7a654588e84a5cf2f5914099638b33547f2b7c", size = 82936, upload-time = "2026-08-29T13:45:40.346Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/7b514f6eb4bf518a9af0285c1a78297467b95fd7d1ebab08b41f8889ab2f/websocket_client-1.9.1-py3-none-any.whl", hash = "sha256:19a871b81d0022589dd9e8e1b1891e0516d32113837dfb987d7691b6a425a1e2", size = 95460, upload-time = "2026-08-29T13:45:39.255Z" }, ] [[package]] name = "wrapt" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/b0/c1f5a970721f06b85c0cd5142e0ff8fe067708abd779b0c4f4be7d61d09f/wrapt-2.3.0.tar.gz", hash = "sha256:681a2d0eefd721998f90642762b8e75c2159ec531b20ad5e437245ea7b06a107", size = 131509, upload-time = "2026-07-28T06:06:14.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/7f/cfd9bc4b1f5e424eeea83d0493e43f3b1b02707ce8e50c47945873982bd5/wrapt-2.3.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d221a6e6ddd302b8397433184e96b59f259f50024b854db1c411a881586b6b8", size = 172381, upload-time = "2026-07-28T06:04:37.674Z" }, - { url = "https://files.pythonhosted.org/packages/cb/89/ff7814f6eb6856b479946117d1138a2fbb46cdb6b1f379db359056c69743/wrapt-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:392158c9a7f2ab1b8699418bfc0fe6f83548788c418b27d7bf2019ad3405cebb", size = 174120, upload-time = "2026-07-28T06:04:38.987Z" }, - { url = "https://files.pythonhosted.org/packages/12/1e/8eded8615d39e3ce81f626937a3a87b280a2a86239a2bf14a4b4bb345034/wrapt-2.3.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e5301c35cf75655eb33498f2bd6ae8703ca19940e3167dc9cdf740c712a39c60", size = 163035, upload-time = "2026-07-28T06:04:40.361Z" }, - { url = "https://files.pythonhosted.org/packages/35/ea/a0af2d9da62897af2a055484920de05dade30d2ba2c0d65cbdea875d3d8b/wrapt-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:418f54bb09d1762db02c7009b4051149893af3153a87f92d70356703c11eea02", size = 171887, upload-time = "2026-07-28T06:04:41.614Z" }, - { url = "https://files.pythonhosted.org/packages/7e/dd/63cd4c864c65ef4906df64bd2d378f4a62b54f28063f282dfb3bf93caead/wrapt-2.3.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1598becd30f8f2777d18564064eb4f4dbe1ab0e05a8f09786d0ef505ac782bf3", size = 161113, upload-time = "2026-07-28T06:04:42.864Z" }, - { url = "https://files.pythonhosted.org/packages/ca/ee/82f1fc9e431b5c2c5a6d201aa865dbeae3984c311c6d11a185f0c8367cf6/wrapt-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3da470536bf9645143323dd41b32db55c6f4304ad382094c1a1da8a92061e10d", size = 170530, upload-time = "2026-07-28T06:04:44.212Z" }, - { url = "https://files.pythonhosted.org/packages/d8/90/343bb5d0f1f9669bc252a6073f085b4abf862511bd5c9c9eaec754341f1d/wrapt-2.3.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9045917809c63fdf7abe3a2ceaed3d670b8ee4500ddd9291192d30aeb34467c5", size = 170350, upload-time = "2026-07-28T06:04:52.187Z" }, - { url = "https://files.pythonhosted.org/packages/59/f8/13b79a392930bd0dd6b86cbfbfe1c40944110456e1dc6d809e5c46ece904/wrapt-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54ca1d5573f69b5fe1d74f1f65799c68015e82f685efec9fd8cfa40a094c44d0", size = 170022, upload-time = "2026-07-28T06:04:53.599Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fc/4f1b6918f5290db959d6e0c07f77385d87cede29c39c9cf8f145e9c82954/wrapt-2.3.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:242b60c21e30866e6a2fa606c612b47c553fa60c0eaeeeb7797fb842ac0ce609", size = 161043, upload-time = "2026-07-28T06:04:54.936Z" }, - { url = "https://files.pythonhosted.org/packages/01/e1/45d3cf74414780bdff6d0380467e003f6eb0f028b6c9403db868dbc7209c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3f3d7ec0a51fbfe00d3aef047641ff2c58b25565b4717fc1f90e050be01cba8", size = 168576, upload-time = "2026-07-28T06:04:56.261Z" }, - { url = "https://files.pythonhosted.org/packages/f3/73/2fa58dd97f191c997755e2c6d569a68f0c433db4e4b36099bdd7227b6cac/wrapt-2.3.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:261f53870cd4fb2bf38f9f972c56c728fd224cb7c65721307de59d9e7e6741ae", size = 159140, upload-time = "2026-07-28T06:04:57.754Z" }, - { url = "https://files.pythonhosted.org/packages/29/a8/08a56e2000a8816d449dcbad8c8b081697acbbd490821ceca0f9d8e8d20c/wrapt-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8159ec0b0cb7608175eb150de94c19e34f4d47ac655f5ca9baf45df6b688ffd3", size = 169263, upload-time = "2026-07-28T06:04:59.161Z" }, - { url = "https://files.pythonhosted.org/packages/fa/1a/4a796ff7adb26ada6d4b758c94d47a38320b085e7099afc088efbbcdb006/wrapt-2.3.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:646d20d413ffcd1b0a2f700076e2d0252d872dcb7754860a73e45a59ea883614", size = 207168, upload-time = "2026-07-28T06:05:07.256Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3e/d7777776806c579b761bac2f91721dda9f04c7a1b380213c5935cc750ae6/wrapt-2.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:379f670f45b7bb8993edd9f6fc36c6cc65edb81cffa0b504be34acb0303fff0a", size = 214351, upload-time = "2026-07-28T06:05:08.945Z" }, - { url = "https://files.pythonhosted.org/packages/63/27/2d64d394df7bf181955b3bb562bf33c4492fb4be113f53071106d43ad8b5/wrapt-2.3.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6208f302f110295d64b22a7ac96500c791bf492dce4366e622e4912b077c9687", size = 199020, upload-time = "2026-07-28T06:05:10.418Z" }, - { url = "https://files.pythonhosted.org/packages/3e/3d/fb31d3db7d9834d265fb1a27a2adf0ddf51557c67458c97b22439ad6ae3d/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ed635a9ca4f3a5a2b900c10c69e823373bc00ebc114b459383596d3487da3570", size = 209969, upload-time = "2026-07-28T06:05:11.983Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d1/8724b5da582e62070dc9bf4d8bf1972f317297eefd7ba1f2b5c6393ccf6c/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e3b9eaa742ae7a0aaaaad4ca4b69469d757af2d6e6663ef1dadc47adec0aeb41", size = 196324, upload-time = "2026-07-28T06:05:13.557Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5c/3d9ef411149543016ee6bcf3af707f787cebd946527452b94bf122e9b7b4/wrapt-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0f7284f88f4833705132d06d3b425a43095c2cbd07c58166aac3ab646ba12a4", size = 202610, upload-time = "2026-07-28T06:05:15.048Z" }, - { url = "https://files.pythonhosted.org/packages/f5/73/31c1bf0f3384062751c2094dadb314916d70aa9b6bfd26d994b4a7b393fa/wrapt-2.3.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:141ed6211286a9660d8d6702de598b43f0934b4f0eda16393f100a80f501d945", size = 170187, upload-time = "2026-07-28T06:05:23.904Z" }, - { url = "https://files.pythonhosted.org/packages/ed/25/fce087d54b79b8905f3c3c9dd5f454bbd8d8acb80b960c4a6aee5b4659b3/wrapt-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e49885a62ec4ee854d1b9e6371fda6afd219917225752abf729a3f36d4df9a5", size = 169288, upload-time = "2026-07-28T06:05:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/c7/30/0d09e6dddc6b7a7230ac77f50254b5980ab4fcd22976f72f8cc8a0404458/wrapt-2.3.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d6159c9b2fefec02314e1332dbbbfaf960e369dfd26bcf7f8b258b5732065b3", size = 160932, upload-time = "2026-07-28T06:05:27.022Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ca/0913af0d2ec0c43865d32d615f518fea66c13c5c930e489e9b0de248e9a8/wrapt-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24da48596326ef8e448cfa837b454f638713d3531262375f00e5a9681682fc07", size = 169017, upload-time = "2026-07-28T06:05:28.501Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f2/3d1e47ea81b822210f5df1bf942fd90780a75c055243d569b664529dea88/wrapt-2.3.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cd3a2edf0427013736b8127955cec62608c56e53ea47e82812ea32059cda407f", size = 159065, upload-time = "2026-07-28T06:05:30.01Z" }, - { url = "https://files.pythonhosted.org/packages/43/a5/ef2066ced8e5fca204e2b361e9708e36555b40949c583d997ea3b590817d/wrapt-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fa0df3bff4e7ce45759f33fd39335fe2f60477bb9ecf7b8aa41e7d07ee36a23", size = 168821, upload-time = "2026-07-28T06:05:31.649Z" }, - { url = "https://files.pythonhosted.org/packages/63/61/50d511c0dc5105563849e86daa3e16ac7feef699f79fb05af45ea70107d5/wrapt-2.3.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:85de890ff968196e92dd1ae73a9fb8970495e7650a457b1c9ef0ac3dd550bce2", size = 207171, upload-time = "2026-07-28T06:05:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/3f/59/9b538cf7795217e810699d16bc88b96a830d9b5c403eb2ec2db6b5f2ae81/wrapt-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50f416b74d092bb9f41b424e90dd457f365f7ba4b11de62a23679769a21bd85c", size = 214329, upload-time = "2026-07-28T06:05:42.287Z" }, - { url = "https://files.pythonhosted.org/packages/b3/28/9935d62b1499e5c8b3d191e99ba4eb31ca237a0b699142011a837e9dc7ea/wrapt-2.3.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39febbee6d77301d31da6996b152ce52452da7c7ef72aba10c2fa976dff9c295", size = 199079, upload-time = "2026-07-28T06:05:43.958Z" }, - { url = "https://files.pythonhosted.org/packages/2b/01/4446b80fa2ffa47a3449b250d004ba1c1937f07f64a179608fec735df866/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:93513bec052c6cd987f9f580c3df068c8bc4ebae6543736be3ca7ec5959cafcd", size = 209992, upload-time = "2026-07-28T06:05:45.677Z" }, - { url = "https://files.pythonhosted.org/packages/d4/07/56f26c9f9979586a021e8148747004aba4498f49458c90b0502969b904e1/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:729126e667da34d251b8ebf8a45ef0c5ddadc21542b3d6e1abf4259ece6508df", size = 196334, upload-time = "2026-07-28T06:05:47.608Z" }, - { url = "https://files.pythonhosted.org/packages/8b/41/6d7bcc895b0f28b2250e10908f060687b9165429dcd7f22ddb3d4c031b74/wrapt-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:626b69db2021aa01671ec7bbc9740e558522bd44c18cf2ce69bf3d666a014109", size = 202644, upload-time = "2026-07-28T06:05:49.183Z" }, - { url = "https://files.pythonhosted.org/packages/00/39/3daf9f47be208606586de4568ba6713db53ebc8fd7a575aea1fe57983b69/wrapt-2.3.0-py3-none-any.whl", hash = "sha256:d8c7ed08477429752b8c44991f40ad7838b18332a160698740a6bfbc10d998a2", size = 61866, upload-time = "2026-07-28T06:06:12.9Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/65/ba/8dc25478ed234dacc7d83c671634f347d0bdfb65bf0502f41879cf2f15a9/wrapt-2.4.0.tar.gz", hash = "sha256:7082fc1f94b020ac275870c4af71b09cff22876fe6e9c4c0ad01ea21d217b288", size = 161179, upload-time = "2026-08-30T04:41:51.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/fd/b20e3cb3cab35131b515edf18e8cd777dff680fc76fc00919481f4e536af/wrapt-2.4.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7dbbdbfdacb85c2d962fa52db791c77943fd777d600d74c95af2d53b32f5a94", size = 227806, upload-time = "2026-08-30T04:39:45.264Z" }, + { url = "https://files.pythonhosted.org/packages/08/75/c8dfba5e0caf17cd0718a0cbbe76cb85e637a2d65183fb728232419f6fca/wrapt-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39cd68df4dff79f5336f9c745c06259d204bcb42d504040c9c91eac9e2abb39c", size = 229004, upload-time = "2026-08-30T04:39:47.068Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/d4853fbd33e5860b10d5aec690f563547a92a82e61fb8bb2d4ece1ce3570/wrapt-2.4.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2a9f1a2f75bb95257cc5744e255e10a5a86e923f328b40ad3dbf9d8d03430013", size = 208934, upload-time = "2026-08-30T04:39:48.73Z" }, + { url = "https://files.pythonhosted.org/packages/a3/66/23d0e8de9b411fd198af5121627587563657370c8d509fbe5ea8adb3df79/wrapt-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8763ad01e3725b7751a4575f38bbcc19c0aa0822fec91c5c5bd21ce3ce7e1d2b", size = 225709, upload-time = "2026-08-30T04:39:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/3b357bc90530d510ae59ae7ac48265c482ae899e47637ca4436645688b40/wrapt-2.4.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9125c6dbe8b88c00dd8ef4fc1e55757e8eb4720b6b2b2cc610a45bd32bd28c57", size = 207090, upload-time = "2026-08-30T04:39:51.78Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0c/d8a5c6dbcc2d221308223bcea4130c6332454a855cb4dbd5dcb2360b13b2/wrapt-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28f5de1526831b8f173889a436e289fe181ede8c66c9feb669d1aca8fd602eaf", size = 216269, upload-time = "2026-08-30T04:39:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a3/a3b5cde1cd06e04b6e95134eb3187a0a7da607a530e7795b221d4e4fa819/wrapt-2.4.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6436e2bda993a3eb69a1b317fc831c8ebcafb5704c390859ebd49f81218c4bbb", size = 225787, upload-time = "2026-08-30T04:40:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f7/d100f6c348b7669f19119cf890dcd4764623e2233af065586d110e0cd99e/wrapt-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e084558fbd112d2e1e34b0f5c71e45a3405bdad51a17150368a959bcf6697964", size = 226649, upload-time = "2026-08-30T04:40:04.647Z" }, + { url = "https://files.pythonhosted.org/packages/52/c6/3af8df515d5d7e92306957536f3468c6bdfecbe3659f99dbf09a468c2c4c/wrapt-2.4.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e78c947e18fadfd690c9420c30a96d221feeb93fc8f1cc00509b370ac16c3114", size = 206760, upload-time = "2026-08-30T04:40:06.332Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c5/40d355552bd3eb6c5186e26051c19b573d24d7896de42caa7937d6b5ca9f/wrapt-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:08d8378c4514ac8dcc0ace76044cf87a873e6a52b5e6109834c8fb9037f4441b", size = 223467, upload-time = "2026-08-30T04:40:07.829Z" }, + { url = "https://files.pythonhosted.org/packages/40/ab/d198eebdb39f0d7e182e771e590a36673489cd58cebdad8aa273dcf28e04/wrapt-2.4.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:93180c2199784dd6a1075b33f9ed636bd0966821edbece6b3d5379b1c4f0bb7d", size = 205358, upload-time = "2026-08-30T04:40:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0e/974a60672ad507d39a3d8a1c6351ef37fe65b07240d000ceba5d2b83e9e9/wrapt-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d5e5eb76fb87e62752af751d2dcd9d1cd986b12037d2e1363d109ba716029e8", size = 214654, upload-time = "2026-08-30T04:40:10.923Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e3/8fdc9eba0e6cbbfe8303e1e807d734691309a27970b2ea458d099f1a46b0/wrapt-2.4.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3a69161cae7f0dca44c89c1d14146b4a0508a0c3cad98b3f2db1f4e9016c94ba", size = 228775, upload-time = "2026-08-30T04:40:20.604Z" }, + { url = "https://files.pythonhosted.org/packages/f4/77/4ac5882abfb29bf9821c5fa5cf9f30241a194e0f47faa2682b9b29765278/wrapt-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0536f5d85ff6a157ebe7e0fe08c5479943742cf1ce59569075a66159efcbc495", size = 229029, upload-time = "2026-08-30T04:40:22.186Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c5/8a3608311a02faf3e5c072da38d06a7c623150fc258e29f18fe377d91703/wrapt-2.4.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f041ed6a4d571010944bd6cfad9072db463e1851877b6d3227467a44af37456", size = 210436, upload-time = "2026-08-30T04:40:23.953Z" }, + { url = "https://files.pythonhosted.org/packages/de/90/e0cbc43f435fd39df25460e9f173e7b96f3dac5c7f66be41c7227166f021/wrapt-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7fed45dbadf5d98a52bfff9624d3cca00affeb9543d493c9632b7a53cdd35c9", size = 226586, upload-time = "2026-08-30T04:40:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/81/6c/7e5f2143228635ec139ef6df733dc477049f7d96a0c49deb23944a73ed6a/wrapt-2.4.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cc2e7c7b6032e11a2b367a9baadaf0c5241feff2d8205260d87f1aa6dbdf84b", size = 208880, upload-time = "2026-08-30T04:40:27.128Z" }, + { url = "https://files.pythonhosted.org/packages/10/16/1de84402bb7a0916e10739bf6586e031244172b299e87c8cff2a04baf9ff/wrapt-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:72826910a1cf5a081234720fd43011304b899acfee219af49148155b4d795533", size = 216689, upload-time = "2026-08-30T04:40:28.844Z" }, + { url = "https://files.pythonhosted.org/packages/84/46/8ec4941d0abbb010df7caf0a34840ca0128177389843b0f5ef2f9ee48ac5/wrapt-2.4.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e9f8017443595870aa31f46125553a5c55ce95a26a267b96261baee6ba566d83", size = 269389, upload-time = "2026-08-30T04:40:38.212Z" }, + { url = "https://files.pythonhosted.org/packages/14/b5/a0ae1b431cc1f49a545d32b8b678a5788c50583ecf0ecb85dc0c7f95b4f6/wrapt-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:328eb2d978ca3a6ae25f8d8fe560bf8f4bc9778b5932e7b142664eef05b92e8f", size = 281081, upload-time = "2026-08-30T04:40:40.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/24/dfaf53dd3bdb0703524a9367b48e2a64ea86433fcc854b5f14be6a8e0e39/wrapt-2.4.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a057d376d994da6bd1bbf955ecfda699aa7353826f98847f5605e1801abdfd4", size = 249637, upload-time = "2026-08-30T04:40:41.657Z" }, + { url = "https://files.pythonhosted.org/packages/3e/27/bdd82044d7503c2bfa78afcc89881f82a1b82b5d2013aabab853d339ce2a/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3367a5212212c9393e0d3ca6ae029b3a8fa40c5896e4a985d43fe8a4b8322f0d", size = 275322, upload-time = "2026-08-30T04:40:43.408Z" }, + { url = "https://files.pythonhosted.org/packages/c4/82/04f4228eb3fb348d660dd1ea7225e53665b1809df2273ff4861d4d33b741/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c4fca1e63af6675af3df7cdfcd5a0c878b5e655c7e48611ced9dc8d62183a11d", size = 247292, upload-time = "2026-08-30T04:40:45.457Z" }, + { url = "https://files.pythonhosted.org/packages/a2/20/67b2968fa9200458446c51b36a435adb6906083428b70fafb4caf92d4dc2/wrapt-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:694005fdc3002ade0f21641408c588028abde03c85961f3ba7727d8bead3ed6b", size = 264586, upload-time = "2026-08-30T04:40:47.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e8/46571e1218d0494604a7aadc4c898c738c4b179052327ee1e57e278cebd6/wrapt-2.4.0-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd9a4be6785295e471f71efdf5682bd11d5b822b9665e6e1b4844917cf2f7ac", size = 229230, upload-time = "2026-08-30T04:40:56.703Z" }, + { url = "https://files.pythonhosted.org/packages/78/2e/0cab15fcaec56096a5734feace3620bc01edc885653be04bd756f84a6784/wrapt-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75529a2fb569a671cf162f762c1b576f569f571b55ec7f3481258ca842ba507f", size = 229444, upload-time = "2026-08-30T04:40:58.51Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9e/a92c049371a2675f98a0381ab2951f984866d1ba4de0e0771d6a31fdaa2b/wrapt-2.4.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66e7512c0d324cc37bba1def2be1fc365cbb685d3aa393a8f6f4d2d00202881d", size = 212482, upload-time = "2026-08-30T04:41:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/ee/3b/8b5b57d0ff24edcd3421dbaeb4e94c89be3616824e47708f4e13f25ae3d7/wrapt-2.4.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:5f3bdfc35c83b562fcaebc0f24593045e5ed9f3b633adafd35222718a0ec38fa", size = 227017, upload-time = "2026-08-30T04:41:01.918Z" }, + { url = "https://files.pythonhosted.org/packages/0e/20/124b40bfd9585848db5a5aa6741d0c8dbf378dd995c6c2d95f090d9cf540/wrapt-2.4.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:d5f45bead708e2c0014be5e98531ce7202916b098a208c7be83c6ceb0a2559fa", size = 210498, upload-time = "2026-08-30T04:41:03.617Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/89db9d5a80a9f2af52b24bdfdb5392be80bc0f0fd39fc39d1aab72afd0bd/wrapt-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d294576fddac636589e4deccfe782e8f429da10f167c1985c4d51071de3672b7", size = 217046, upload-time = "2026-08-30T04:41:05.473Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e1/e2437f17f2a1ec292056e2fcafe1248269ebc39502f2ffe79424bf86f8a6/wrapt-2.4.0-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:75944792cf6b99262d649d55710bf5901f7013fbb212c7a1d736b97a20517607", size = 269421, upload-time = "2026-08-30T04:41:15.238Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d0/c98d6548dc4c7d12ab9baa192234ca1a57e141afd283252b448faddbd9ef/wrapt-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:648d1d4f94e8a0a1656675c755f40d2f0ee5fe92c449ab45326f4ecc2738cbe8", size = 281452, upload-time = "2026-08-30T04:41:16.939Z" }, + { url = "https://files.pythonhosted.org/packages/a3/57/673168e00aa03725148ce621ed201b75df4e787a57acd48fecefd2725600/wrapt-2.4.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a112a1bfdd2621e4344cb0a32dbaab80636b32dac1b055d03fbb2a67d806d1db", size = 250358, upload-time = "2026-08-30T04:41:18.716Z" }, + { url = "https://files.pythonhosted.org/packages/78/0b/f2e576de5bf53ef5b578470104ea93f33e273a704c825131bc1719fffc42/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0972cd025f4c86fa2d8abd953d9f875779935343af58b4ce019ff89573fc65bd", size = 275654, upload-time = "2026-08-30T04:41:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/9347b2e236346b1ba4cb28b82b205b8a377bb2da9417cb81bbe3d25816d7/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c246aaed719dcdb62eeb7b8d9306a6237777226ef3baad35919c4ae134c91ce7", size = 248662, upload-time = "2026-08-30T04:41:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/a5/36/3b84d9e1ac8393bf2c94272760a2d361dc394ac30301e6d6dbd6583ade2d/wrapt-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:1656de3835f760781c9b974bce07d8c04edb9c9ad7ad67264aee69cd68a1db09", size = 264813, upload-time = "2026-08-30T04:41:24.116Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/fafe0002f572ced999c792cfe8b05d39269c63d8193d15d25bd828bcad7a/wrapt-2.4.0-py3-none-any.whl", hash = "sha256:18aabd9301d06026f5900538051773d6f87f65ae02cdc60de482df978513dc0a", size = 73713, upload-time = "2026-08-30T04:41:49.805Z" }, ] [[package]] @@ -4321,20 +4379,20 @@ wheels = [ [[package]] name = "zope-interface" -version = "8.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/dc/50550cfcbb2ea3cbca5f1d7ed05c8aa840f831a0f2d63aec0a953f7c590e/zope_interface-8.5.tar.gz", hash = "sha256:7a3ba1c5877f0f3e3906b02ddf793abed2becc2948116414ce0e1dd820b68d6d", size = 257957, upload-time = "2026-05-26T06:50:14.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/c3/005032195ff3b210c139b7c560ed5c534e844b0907d8e44d2b3d8919305e/zope_interface-8.5-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:e6347b8d8d12c5eca6502450a92be30079b7acfade2c4f693efa0deb8871b06e", size = 265296, upload-time = "2026-05-26T06:49:29.741Z" }, - { url = "https://files.pythonhosted.org/packages/c5/66/1036543d6a66bc04c19df3cf650f3ad938a002ab0a443c24e23e8de5e8b9/zope_interface-8.5-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e970dabea777a24b0b0bbf9dae3ab75ce8b2d8e948edf4875627034b21f3560", size = 270689, upload-time = "2026-05-26T06:49:31.767Z" }, - { url = "https://files.pythonhosted.org/packages/30/4c/8b56259558cace4414e753ca6740396a1f59d4a95ddb55b4658600408670/zope_interface-8.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f0b48ccadaa9839e09ff81e969703cecb3f402c813bfe8b958652e699bea69f5", size = 270280, upload-time = "2026-05-26T06:49:33.489Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a5/692a2b8d70f78e848793231d5fae5fecbf8d0cccd73430fdc34802a6d3c1/zope_interface-8.5-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:7849ad8fa90763cc1087f4dda78ca3a233e950b3e08fac7079297c9cafbbd7bb", size = 265191, upload-time = "2026-05-26T06:49:43.449Z" }, - { url = "https://files.pythonhosted.org/packages/70/8d/454a9cfc7a050c394ab4f11b3371f7897828b7415e096afff724637e65e0/zope_interface-8.5-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5578c9421ca409a1f39f153d6f7803e4cde01da592ec75a9ac5e1b777d18d33b", size = 270626, upload-time = "2026-05-26T06:49:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/51/8c/db8409cfa3575b8e9b4800babd7d49f8228433cd1f0c56814bd0ada49c33/zope_interface-8.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e1bd7d96b4ca5fa311f54c9eac16dce4886b428c1531dbe06067763ccdf123b4", size = 270444, upload-time = "2026-05-26T06:49:47.025Z" }, - { url = "https://files.pythonhosted.org/packages/32/67/bc8b4f465d388039255003e230c284a175cedf1203c692f23cb7bff64efe/zope_interface-8.5-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:3090e3a663d20194756a59a272e0c8508b889341e31d5894223331fe6b4f9b21", size = 266827, upload-time = "2026-05-26T06:49:54.873Z" }, - { url = "https://files.pythonhosted.org/packages/a7/eb/37d05b935ede53d79690fecc8d201440084418e590bcfc05f384451c7593/zope_interface-8.5-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9342fb74e2afefdb081bf1df727d209ea56995c6e13f5a0540e6d7aff4beafb8", size = 270139, upload-time = "2026-05-26T06:49:57.116Z" }, - { url = "https://files.pythonhosted.org/packages/8b/0b/fd0c54579e2ce8dc6cf1a757903f3374bc6fbda929a46af9e0f53cb0e5f0/zope_interface-8.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6c54725d818f1b57a7efb8b16528326e1f3c257b602b32393fd255c45af8799d", size = 270338, upload-time = "2026-05-26T06:49:58.698Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/d97430abd5ae9677e8b9295b58720c0064a5b557dbb6b8bf5928484cf0d8/zope_interface-8.5-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:14b0e9799351d4c34fe99afd67f0cdd76e55ba15c66a98699d5fc22ea8241e08", size = 294905, upload-time = "2026-05-26T06:50:07.384Z" }, - { url = "https://files.pythonhosted.org/packages/41/ec/a0f8f3dad6e74992f4654bdd94802be0929eabca7b871cac3b6fbb5e961b/zope_interface-8.5-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0cd6a732ac84b94eb1ef9222a117347a27efd294ee16810ffdf7ecd307677ed5", size = 300885, upload-time = "2026-05-26T06:50:08.997Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/6881b48803a0ee8d23eb5efa30fce3ed218a2bd9de5758ce489d224fee81/zope_interface-8.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:798b7c87d0e59a7d5d086d642208d0d8700ff0d55c4029134b3c479c3bfb110f", size = 304672, upload-time = "2026-05-26T06:50:10.563Z" }, +version = "8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/39/a8481b926e42c44a6fcc670904f8251469ec42edbff1ba066719ca1e7fb4/zope_interface-8.6.tar.gz", hash = "sha256:b40ef9b4873afb5d0dec02b8d2dfde1cf18c72337b60c99cb735961e0bac05c0", size = 257973, upload-time = "2026-08-20T11:18:08.717Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/e1/b2d78ecb8aec59114111ed8c25894c0421afecc5e89b36fc356e2b07a607/zope_interface-8.6-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:780a66db884c0e2b0e6b34b4900f86916945a7c03d3be40ec845b051fcc052cd", size = 265308, upload-time = "2026-08-20T11:17:20.02Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5a/126eeee4da016f5cca4db2297496069d5f1ba901fb53ebf104f9c087a113/zope_interface-8.6-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9217b1123f6aeec9ddf1789bffd83da3123546d551c164a99f862a5d1f5ac0f8", size = 270703, upload-time = "2026-08-20T11:17:22.016Z" }, + { url = "https://files.pythonhosted.org/packages/05/89/7767a6f9b0bb41a4d3777e8f93bfeb1b9a23ea643f83ce96163d9d672c8b/zope_interface-8.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:28b68c24131545c1d13fd2178bbd065e67f09db885d8426adf1fbdf2b6b66372", size = 270292, upload-time = "2026-08-20T11:17:23.905Z" }, + { url = "https://files.pythonhosted.org/packages/4c/2d/5b4dbbe618b816f626f2a640fcd9911a461e3733a608c4043a8cc79c12b3/zope_interface-8.6-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c2bf932006229788d6bb41963dfc0345cba6ee24141a39316bd52a283a7d115f", size = 265203, upload-time = "2026-08-20T11:17:33.059Z" }, + { url = "https://files.pythonhosted.org/packages/79/96/c02befafb8e5d3c92898aa02fffca94d164830013fd0a50c4a652a728712/zope_interface-8.6-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:09522cdc6a77376bc36988b531db3b568c8cb0b6ca7286d8316aab283888770f", size = 270637, upload-time = "2026-08-20T11:17:35.167Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c4/d61b18724597ca62c1a3a753370fff7b76f43c01b44e9a13c18e2300eaf0/zope_interface-8.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:edf1bd7ed576319241b2b314eaa549cee3e3e0f81f46911086b387d03a303ad3", size = 270456, upload-time = "2026-08-20T11:17:37.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/56/01f84b4e966a32088e9076b1e7b2afa310f52bf9b9a077d2958cf66e81aa/zope_interface-8.6-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a91eb220d9ae6aa6d746d6dac5b4db35b1417903301b3315ba3275b19570be0b", size = 266840, upload-time = "2026-08-20T11:17:46.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/40/2a644e32cd6f0516e7df1fc0c58e544a8cc11ba06b0d55d308519b02459d/zope_interface-8.6-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3f7f6da49911ffe75ae3f7a9a45619f205420cc6578aff02f8ca29ed1de10f14", size = 270145, upload-time = "2026-08-20T11:17:48.195Z" }, + { url = "https://files.pythonhosted.org/packages/1e/18/02ebd81feff11a2766159fcb49c5b773fef5ae4414c38fb19114aad9e961/zope_interface-8.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef15a2f6258f809334a19c1fcce64648813066ceebe3f3f6077871483fd0f50d", size = 270351, upload-time = "2026-08-20T11:17:50.07Z" }, + { url = "https://files.pythonhosted.org/packages/a9/96/ac48a6b7cfe972e4a9b0d7ec8b9f36a7956cc95d72029f0013ff096c55af/zope_interface-8.6-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5dbe120cfcfc8e6aed418f340c3d1ad4072253e17176503e363ddac27fcb2ac6", size = 294916, upload-time = "2026-08-20T11:17:59.952Z" }, + { url = "https://files.pythonhosted.org/packages/a2/54/4df4bb0b1aace2298386375ab2fb752378683b558d2db713e25c40a3e96a/zope_interface-8.6-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:27e6de8e593736210d2a9f1bbf766a5653aa4819c184f864ab9d1f8bd3590a60", size = 300898, upload-time = "2026-08-20T11:18:02.224Z" }, + { url = "https://files.pythonhosted.org/packages/08/9c/0c8c80c1eeb62ac0c3ed1f51ad8cdd6da9373c53247c659c49f0ea29f742/zope_interface-8.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:66ab8c5d8820aa378968c16b7a3cb051aca342eafa649c9a363182f572d75ccb", size = 304684, upload-time = "2026-08-20T11:18:04.105Z" }, ] From e946f11f6d4fc985821f9c262e3f928a8b81a44c Mon Sep 17 00:00:00 2001 From: Cail McLean Daley Date: Mon, 31 Aug 2026 04:18:38 +0200 Subject: [PATCH 03/36] feat(testing): inject controlled PSF-model error in make_data (#828) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make_data renders the galaxy with the true PSF and, by default, hands ngmix that same PSF as the model stamp — zero deconvolution error, the one shape systematic the digital twin could not previously probe. Two optional knobs let the model stamp differ from the truth: psf_model_fwhm_ratio (size error) and psf_model_shear (shape error). Defaults reproduce the legacy output byte-for- byte; injected error perturbs only the PSF model stamp, never the galaxy image. Claude-Session: https://claude.ai/code/session_01C9zx7LC72tPMhNGtKnADpp Co-authored-by: Claude Opus 4.8 --- src/shapepipe/testing/simulate.py | 40 ++++++++++++++-- tests/module/test_simulate_psf_model_error.py | 48 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 tests/module/test_simulate_psf_model_error.py diff --git a/src/shapepipe/testing/simulate.py b/src/shapepipe/testing/simulate.py index bb1c7823b..76933cc80 100644 --- a/src/shapepipe/testing/simulate.py +++ b/src/shapepipe/testing/simulate.py @@ -24,6 +24,8 @@ def make_data( psf_shear=(0.0, 0.0), wcs=None, return_centers=False, + psf_model_fwhm_ratio=1.0, + psf_model_shear=None, ): """Simulate an exponential galaxy with Moffat PSF. @@ -55,8 +57,7 @@ def make_data( Ellipticity (g1, g2) applied to the Moffat PSF. Default (0, 0), a round PSF. A non-round PSF is what the PSF-leakage / additive-bias guardrail needs: the PSF carries a shape that an unbiased deconvolution - must remove from a round galaxy. The same sheared PSF is used both to - convolve the object and as the PSF model stamp. + must remove from a round galaxy. wcs : galsim.BaseWCS, optional Uniform drawing WCS. Default None means ``galsim.PixelScale (pixel_scale)`` with the legacy world-space sub-pixel shift draw — @@ -74,6 +75,24 @@ def make_data( ``galsim.PositionD`` in galsim image coordinates (1-based; stamp centre is ``(img_size + 1) / 2``). Default False keeps the legacy 6-tuple return. + psf_model_fwhm_ratio : float, optional + Multiplicative size error injected into the PSF *model* stamp handed to + ngmix, relative to the *true* PSF that convolves the galaxy. Default 1.0 + (no error: the same PSF object renders the galaxy and the model stamp, + byte-for-byte identical to the legacy behaviour). A value != 1.0 makes + the model PSF the wrong size — over-sized (>1) over-deconvolves, under- + sized (<1) leaves residual smoothing — so metacal deconvolves by a PSF + that does not match the one in the data. This is the controlled-PSF- + model-error knob: it tests whether metacal's response correction (which + is computed with the same wrong model) absorbs a deconvolution error, + the one shape-measurement systematic the zero-error sim could not probe. + psf_model_shear : tuple of float or None, optional + Extra ellipticity (Δg1, Δg2) added to the PSF *model* stamp on top of + ``psf_shear``, relative to the true PSF. Default None (no error). A + non-zero value mismodels the PSF shape, injecting a controlled additive + (PSF-leakage) error into the recovered shear. When both this is None and + ``psf_model_fwhm_ratio`` is 1.0 the model PSF *is* the true PSF object, + so the output is byte-for-byte the legacy result. Returns ------- @@ -132,7 +151,22 @@ def draw_shift(): ), ).shift(dx, dy) - psf_im_ = psf.drawImage(nx=img_size, ny=img_size, wcs=wcs) + # The PSF *model* stamp handed to ngmix. By default it is the very same + # object that convolved the galaxy (zero model error, byte-for-byte). A + # size and/or shape error makes it differ from the true PSF, so metacal + # deconvolves by the wrong PSF — the systematic the zero-error sim could + # not exercise. + if psf_model_fwhm_ratio == 1.0 and psf_model_shear is None: + psf_model = psf + else: + psf_model = galsim.Moffat( + beta=2.5, fwhm=psf_fwhm * psf_model_fwhm_ratio + ).shear( + g1=psf_shear[0] + (psf_model_shear[0] if psf_model_shear else 0.0), + g2=psf_shear[1] + (psf_model_shear[1] if psf_model_shear else 0.0), + ) + + psf_im_ = psf_model.drawImage(nx=img_size, ny=img_size, wcs=wcs) psfs_sigmas.append(galsim.hsm.FindAdaptiveMom(psf_im_).moments_sigma) psf_im = psf_im_.array.astype(np.float64) im = obj.drawImage(nx=img_size, ny=img_size, wcs=wcs).array.astype(np.float64) diff --git a/tests/module/test_simulate_psf_model_error.py b/tests/module/test_simulate_psf_model_error.py new file mode 100644 index 000000000..42992c3e0 --- /dev/null +++ b/tests/module/test_simulate_psf_model_error.py @@ -0,0 +1,48 @@ +"""Guardrail tests for the PSF-model-error extension of ``make_data``. + +``make_data`` renders the galaxy with the *true* PSF and, by default, hands ngmix +that same PSF as the model stamp (zero model error). The ``psf_model_fwhm_ratio`` +and ``psf_model_shear`` knobs let the model stamp differ from the true PSF, so a +controlled deconvolution error can be injected. These tests pin the two +properties the digital twin's PSF-model-error sweep relies on: + + 1. with no error injected the output is byte-for-byte the legacy result, and + 2. an injected error perturbs ONLY the PSF model stamp, never the galaxy image + (the galaxy is always convolved with the true PSF). +""" + +import numpy as np + +from shapepipe.testing.simulate import make_data + +_KW = dict(shear=(0.02, 0.0), noise=1e-4, n_epochs=2, img_size=51, + pixel_scale=0.1857) + + +def _run(seed=123, **extra): + gals, psfs, *_ = make_data(rng=np.random.RandomState(seed), **_KW, **extra) + return np.array(gals), np.array(psfs) + + +def test_no_error_is_byte_for_byte(): + """Explicit no-error kwargs reproduce the implicit default exactly.""" + g0, p0 = _run() + g1, p1 = _run(psf_model_fwhm_ratio=1.0, psf_model_shear=None) + np.testing.assert_array_equal(g0, g1) + np.testing.assert_array_equal(p0, p1) + + +def test_size_error_touches_only_the_psf_stamp(): + """A model size error changes the PSF stamp but leaves the galaxy intact.""" + g0, p0 = _run() + g1, p1 = _run(psf_model_fwhm_ratio=1.05) + np.testing.assert_array_equal(g0, g1) # galaxy uses the TRUE PSF + assert not np.allclose(p0, p1) # model stamp is perturbed + + +def test_ellipticity_error_touches_only_the_psf_stamp(): + """A model ellipticity error changes the PSF stamp, not the galaxy.""" + g0, p0 = _run() + g1, p1 = _run(psf_model_shear=(0.03, 0.0)) + np.testing.assert_array_equal(g0, g1) + assert not np.allclose(p0, p1) From 21570d10e18ccd76d4dfd4c921fb2d2b50dc54a1 Mon Sep 17 00:00:00 2001 From: Martin Kilbinger Date: Sat, 5 Sep 2026 15:06:10 +0200 Subject: [PATCH 04/36] Coverage nexp masks (#797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * removed tmp files * Moved get_ccds script to pipeline runner and library script * class to download eposure headers (for coverage) * coverage mask: scripts and plotter; using cs_util * coverage code: added Mike Hudson as co-author * documentation (in pipeline_canfar.md) on coverage mask scripts and classes * CCD PSF Handler: Added version for v1.3 * fixed coverage plots * header downloads (coverage): account for existing downloads * canfar-changes * PR: remove non-functioning coverage pipeline function * PR: coverage_map_builder; moved import into try block * PR: field_corners_extractor.py; fixed exp number extraction * PR: added comment * PR: added comment * refactor: consolidate field_corners_extractor helpers; fix regex - Extract _expnum_from_path, _parse_header_to_wcs, _megacam_field_corners as module-level helpers. process_single_header (static, for multiprocessing) and the instance methods now share one implementation. - Remove dead get_wcs_from_header and get_megacam_field (duplicated logic that had already been inlined into process_single_header). - Fix re.search(r'(\d+)\.txt') missing its subject string. The new helper also replaces the hard-coded p[end-6:end] exposure-number extraction in run(), so exposure numbers of any length work. - Declare verbose in params_default so the contract is local instead of relying on cs_util.args.parse_options to inject it. Addresses review feedback on #699. * refactor: coverage_map_builder cleanup - Replace hsp_map + 0 with hsp_map.copy() (healsparse supports .copy since at least 1.x; more explicit intent). - Declare verbose in params_default; currently populated only by cs_util.args.parse_options auto-injection. - Fix typo: "Install te cs_util" -> "Install the cs_util". Addresses review feedback on #699. * docs: declare verbose and annotate survey-specific defaults - coverage_plotter, header_downloader: declare verbose explicitly in params_default. - coverage_plotter: comment acknowledging that FootprintPlotter._regions is a private cs_util attribute; refactor to public accessor when cs_util exposes one. - header_downloader: expand the vos:cfis/pitcairn comment to flag it as UNIONS/CFIS-specific and document override. - build_and_plot_coverage_maps.sh: explain the BUILD_NSIDE=131072 choice (matches UNIONS bit-mask pixel scale, ~0.1"), and note the CoverageMapBuilder default of 2048 for lighter use. Addresses review feedback on #699. * chore: uv lock — register healsparse + skyproj as shapepipe deps The coverage feature adds healsparse (map building) and skyproj (plotting) to pyproject. healsparse was already resolved transitively via cs_util; this pins skyproj 2.5.0 and records both as shapepipe's own direct dependencies so 'uv lock --check' passes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvP7uKkfDU2PvWC7YmbuMW * fix: coverage console scripts handle -h; drop dead entry point The five coverage/get-ccds runners fell back to `args = sys.argv` (the full argv, including argv[0]) instead of `sys.argv[1:]`, so under the entry-point smoke test's `python -c "...; run_*()" -h` the parser read the program slot as the integer `-c` option and exited non-zero (`invalid integer value: '-h'`). Slice argv[1:] to match cs_util.args.parse_options' own convention. Also drop the `coverage_pipeline` console script, whose target `coverage_run:run_pipeline` was removed in an earlier commit (dangling entry point -> AttributeError on invocation), and declare `hpgeom` explicitly (imported directly by coverage_map_builder, previously only transitive via healsparse). Together these fix all six entry-point test failures. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvP7uKkfDU2PvWC7YmbuMW * test: unit + property tests for the coverage-mask feature Cover the pure and lightly-fixtured logic: exposure-number parsing, the CCD-list -> unique-exposure reduction, the MegaCam field-corner convention, the multi-HDU header -> WCS split, power-of-two nside validation, and a regression guard for the -h/argv handling of the console runners. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HvP7uKkfDU2PvWC7YmbuMW * fix(coverage): subtract missing CCDs in get_ccds_with_psf The v>=1.4 path built the full set of exposure single-HDU IDs but never removed the CCDs whose PSF model is missing, so every candidate CCD was reported as valid. Subtract the missing set so the result is the CCDs that actually have a valid PSF, and report the three counts. * feat(coverage): extract per-CCD corners with optional CCD-list filter Replace the whole-MegaCam-field corner extraction with per-CCD extraction: for each HDU's WCS, build the 4 corners from the CCD pixel bounds (read from pixel_shape, i.e. NAXIS1/NAXIS2) using pixel edges so the quadrilateral covers the full CCD area. Emit one row per CCD keyed on the '-' ID. Add an optional --ccd_list/-l option: when given, only CCDs in that file (the output of get_ccds_with_psf) are written, so the coverage mask is built from single CCDs with a valid PSF. Resume still keys the done-set on exposure number, now parsed from the CCD-ID first column. Drop the dead _megacam_field_corners whole-field path; keep _parse_header_to_wcs and the multiprocessing structure. * feat(coverage): build maps from per-CCD rows with RA-wrap and pole guards Parse the new per-CCD row format (string CCD ID in column 0, then 8 float corner columns). Add an RA-wrap guard that unwraps polygons straddling the RA=0 seam onto a common branch, and a pole guard that skips polygons reaching |dec| >= 89 deg with a warning (CCD footprints never reach there). Since the CCDs of one exposure do not overlap, accumulating value 1 per CCD polygon still counts exposures-with-valid-PSF per pixel. * fix(coverage): download headers atomically via temp-file rename Copy each header to a '.part' file in the same directory and os.rename on success. An interrupted transfer can no longer leave a partial file that resume treats as complete; the temp file is removed on failure. * docs,build(coverage): align interfaces to the per-CCD flow Fix the console-script name typo (get_ccd_with_psfs -> get_ccds_with_psf) so it matches the docs and script. Harden the build/plot script with 'set -euo pipefail' and document the per-CCD chain (step 0 get_ccds_with_psf feeding extract_field_corners --ccd_list). Update the canfar docs: restore the coverage helpers as shipped, align filenames (ccds_with_psf_.txt, exp_ra_dec_.txt), and describe the per-CCD workflow including step 0. * test(coverage): cover per-CCD extraction, filtering, guards, atomic download Rewrite the coverage tests for the per-CCD design: handler missing-CCD subtraction; per-CCD corner extraction and pixel-shape error; --ccd_list filtering end to end; builder parsing of the new row format; RA-wrap unwrap (a CCD straddling RA=0 fills a small patch, not the complement); pole-guard skip; and atomic-download rename behaviour with a mocked copy. Drop the tests that pinned the removed whole-field convention. * fix(coverage): read true CCD dims from ZNAXIS and resume per-CCD Two correctness fixes in the per-CCD extractor: - Image dimensions: header_downloader fetches fpack tile-compressed headers where NAXIS1/NAXIS2 describe the compressed binary table (byte width, row count), so WCS.pixel_shape reports e.g. (8, 4612) instead of the true (2080, 4612). Read ZNAXIS1/ZNAXIS2 when ZIMAGE is set, falling back to NAXIS1/NAXIS2. _parse_header_to_wcs now returns (wcs, shape) pairs since the WCS drops the Z-keywords; _ccd_corners takes the shape explicitly. - Resume: key the done-set on individual CCD IDs, not exposure numbers. A write interrupted mid-exposure previously left a partial exposure that a -r rerun skipped forever (silent undercount); an expanded --ccd_list rerun skipped exposures whose new CCDs were never written. get_done_ccds reads the present CCD IDs and run() drops only those, so partial exposures are completed and new CCDs added, never duplicating a row. * fix(coverage): single-line CCD list, runner docstrings, plot doc example - header_downloader.get_exposures: wrap the text-format loadtxt in np.atleast_1d so a single-line CCD list (0-d array) does not crash on iteration. - coverage_run: update the extract/build runner docstrings from whole-field 'field corner coordinates' to per-CCD language. - pipeline_canfar docs: add a runnable plot_coverage_map example (the SGC region command) covering the flags build_and_plot_coverage_maps.sh uses. * test(coverage): compressed-header dims, resume path, nexp value contract Extend the coverage tests for the confirmed findings: - fpack-compressed header fixture; assert ZNAXIS dims are recovered and corners span the true CCD width, not the 8-byte compressed NAXIS1. - resume run() path: a new exposure is appended without duplicating existing rows, and a partial exposure (interrupted mid-write) is completed rather than skipped. - the nexp contract on pixel values: two overlapping CCDs from different exposures give value 2 in the overlap and 1 outside. - single-row builder file (atleast_1d/2d guards); single-line get_exposures; RA-wrap strengthened to a positional-parity check (seam CCD vs the same CCD shifted +10 deg agree in pixel count); get_ccds_with_psf subtraction pinned against the real get_all_shdus. * feat(coverage): accept catalogue version v2.0 (9-patch footprint, as v1.6) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MqH9wjbJicVmaQdhZ4kBYr * feat(coverage): v2.0 defaults to 10 patches; -p overrides the version map The v2 footprint is still growing (>5000 deg2 per the 2026-07 Paris meeting, new Dec<30 region); the newest patch definitions (auxdir/CFIS/tiles_202510) include P10. Default v2.0 to 10 patches and add an explicit -p/--n_patch override so the handler tracks evolving patch layouts without code changes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MqH9wjbJicVmaQdhZ4kBYr * fix(coverage): v2.0 is patch-less — read summaries from the run root Martin (PR #812 review): the patch concept is removed in v2.0. For -V v2.0 the handler reads summary/missing_job_32_all.txt and exp_numbers.txt relative to the run root instead of iterating P1..P9; -p restores an explicit P1..P layout when needed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MqH9wjbJicVmaQdhZ4kBYr * docs: state the patch convention per catalogue version (v2.0 is patch-less) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MqH9wjbJicVmaQdhZ4kBYr * feat(coverage): default get_ccds_with_psf -V to v2.0 (patch-less) Align with the collate_star_cat convention (PR #812): v2.0 (patch-less) is now the default catalogue version. Older, patched layouts are selected explicitly with -V. Help text and the pipeline_canfar.md invocations updated; the v2.0 call drops the flag, v1.x shows -V v1.6. Tests set the version explicitly, so none pinned the old default. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WqNFhq9uGpDpprYeuGeb8k * chore: remove accidentally committed pytest log artifact Co-Authored-By: Claude Fable 5 * coverage: drop the v2.0 patch-less path — v2.0 coverage moves to the Snakemake stack Martin (review, 2026-09-04): the v2.0 pipeline is Snakemake-orchestrated, has no sky patches, and will not use the summary class or its outputs — so summary/missing_job_32_all.txt and exp_numbers.txt simply won't exist at a v2.0 run root. The patch-less path added in July was built on files that are not going to be there. This PR therefore lands as the v1.x tool. Reverts the v2.0 work (15401d9, a3aa4ec, ae10176, 46214c7, 1ffcd4b) and restores the pre-July contract of CcdPsfHandler: - accepted versions are v1.3, v1.4, v1.5, v1.6 again; -V v2.0 now fails loudly with "Invalid version v2.0" - -V default back to v1.6 (was flipped to v2.0 in 1ffcd4b) - the n_patch = 0 / patches = ["."] "patch-less" semantics are gone; patches are always P1..P from the version map (7/7/8/9) - the -p/--n_patch CLI override is dropped: it was introduced in a3aa4ec purely to let the evolving v2.0 footprint be overridden, and the v1.x patch counts are fixed history Docs lose the "Patch convention by catalogue version" note and the patch-less get_ccds_with_psf invocation; the two v2.0 tests (test_v2_is_patchless, test_n_patch_option_overrides_version_default) are removed. The v1.x version->patch-count parametrisation and the invalid-version test stay. Everything else from the July rework is untouched: per-CCD extraction, RA-wrap and pole guards, atomic downloads, resume, ZNAXIS dims. v2.0 coverage will be a separate PR against the Snakemake stack. Co-Authored-By: Claude Fable 5.1 * chore: relock cs_util to develop 1b15a55 (restores FootprintPlotter.plot_area) --------- Co-authored-by: martinkilbinger Co-authored-by: Cail Daley Co-authored-by: Claude Fable 5 Co-authored-by: Cail McLean Daley --- docs/source/pipeline_canfar.md | 51 +- pyproject.toml | 14 +- scripts/python/get_ccds_with_psf.py | 180 +-- scripts/sh/build_and_plot_coverage_maps.sh | 95 ++ src/shapepipe/coverage_run.py | 104 ++ src/shapepipe/get_ccds_run.py | 44 + src/shapepipe/utilities/ccd_psf_handler.py | 387 ++++++ .../utilities/coverage_map_builder.py | 372 ++++++ src/shapepipe/utilities/coverage_plotter.py | 348 +++++ .../utilities/field_corners_extractor.py | 493 +++++++ src/shapepipe/utilities/header_downloader.py | 291 +++++ tests/module/test_coverage.py | 674 ++++++++++ uv.lock | 1147 +++++++++-------- 13 files changed, 3465 insertions(+), 735 deletions(-) mode change 100644 => 100755 scripts/python/get_ccds_with_psf.py create mode 100755 scripts/sh/build_and_plot_coverage_maps.sh create mode 100644 src/shapepipe/coverage_run.py create mode 100644 src/shapepipe/get_ccds_run.py create mode 100644 src/shapepipe/utilities/ccd_psf_handler.py create mode 100644 src/shapepipe/utilities/coverage_map_builder.py create mode 100644 src/shapepipe/utilities/coverage_plotter.py create mode 100644 src/shapepipe/utilities/field_corners_extractor.py create mode 100644 src/shapepipe/utilities/header_downloader.py create mode 100644 tests/module/test_coverage.py diff --git a/docs/source/pipeline_canfar.md b/docs/source/pipeline_canfar.md index 96add5758..cfdd16507 100644 --- a/docs/source/pipeline_canfar.md +++ b/docs/source/pipeline_canfar.md @@ -480,13 +480,12 @@ calibrate_comprehensive ```{note} -The matched-star-catalogue and coverage-mask diagnostics in the next two -sections have largely moved out of ShapePipe into -[`sp_validation`](https://github.com/CosmoStat/sp_validation) / `cosmo_val`. -Several helpers referenced below — `merge_psf_cat.py`, `download_headers`, -`extract_field_corners`, `build_coverage_map`, `plot_coverage_map` — are no -longer shipped in this repository; see `sp_validation` for their current -equivalents. (`get_ccds_with_psf` and `summary_run` are still part of ShapePipe.) +The matched-star-catalogue diagnostic (`merge_psf_cat.py`) has moved into +[`sp_validation`](https://github.com/CosmoStat/sp_validation) / `cosmo_val`; +see that repository for its current equivalent. The coverage-mask helpers +(`get_ccds_with_psf`, `download_headers`, `extract_field_corners`, +`build_coverage_map`, `plot_coverage_map`, and `summary_run`) are shipped by +ShapePipe and documented below. ``` ### Create matched star catalogue @@ -517,29 +516,51 @@ If the file `$patch/exp_numbers.txt` does not exist for a given patch, create it summary_run $patch 1 ``` -Now, create the list of CCDs that have PSF information with +Now, create the list of single-CCD footprints that have a valid PSF model. +Only these CCDs are stamped into the mask, so the accumulated map counts, per +sky pixel, the number of exposures with a valid PSF. The list is written to +`ccds_with_psf_v1.6.txt`: ```bash -get_ccds_with_psf -v -V v1.6 +get_ccds_with_psf -v -V v1.6 -o ccds_with_psf_v1.6.txt ``` -Next, download exposures headers +Next, download the exposure headers; indicate (with `-d`) a directory of +already downloaded headers, which are linked so duplicate downloads are +skipped: ```bash -download_headers -i ccds_with_psfs_v1.6.txt -o headers_v1.6 -v +download_headers -i ccds_with_psf_v1.6.txt -o headers_v1.6 -d headers_v1.3 -v ``` -From the headers, the CCD corner coordinates are extracted with +From the headers, the per-CCD corner coordinates are extracted. Pass the +CCD list with `-l` so only CCDs with a valid PSF are written: + ```bash -extract_field_corners -i headers_v1.6 -v +extract_field_corners -i headers_v1.6 -l ccds_with_psf_v1.6.txt -o exp_ra_dec_v1.6.txt -v ``` Then, build the healsparse coverage mask file as ```bash -build_coverage_map +build_coverage_map -i exp_ra_dec_v1.6.txt -o coverage_v1.6.x.hsp -c 128 -n 131072 -v +``` + +The healsparse resolutions (128, 131072) match the bit masks. + + +Use `plot_coverage_map` to plot a region of the coverage mask. For example, +the SGC region with a colorbar clipped to the 1–5 exposure range: + +```bash +plot_coverage_map -i coverage_v1.6.x.hsp -o coverage_v1.6_SGC.png \ + -C -R -20 -r 45 -D 18 -d 40 -m 1 -M 5 -v ``` -Use `plot_coverage_map` to create plots of the coverage mask. +Here `-R`/`-r` and `-D`/`-d` set the RA and Dec plot limits, `-m`/`-M` the +colorbar range, and `-C` adds the colorbar. Building and plotting for a range +of versions (SGC and NGC regions) is automated by +`build_and_plot_coverage_maps.sh`. + ## Extra Utilities diff --git a/pyproject.toml b/pyproject.toml index 8b259deec..5333aa91c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ dependencies = [ "cs_util>=0.2.1", "galsim>=2.8", "h5py", + "healsparse", + "hpgeom", "joblib>=1.4", "matplotlib>=3.10", "mccd>=1.2.4", @@ -39,6 +41,7 @@ dependencies = [ "reproject>=0.19", "sf_tools>=2.0.4", "skaha>=1.7", + "skyproj", "sqlitedict>=2.0", "termcolor", "tqdm>=4.68.1", @@ -63,6 +66,10 @@ jupyter = [ lint = [ "ruff", ] +plot = [ + "skyproj", + "matplotlib>=3.10", +] release = [ "build", "twine", @@ -73,7 +80,7 @@ test = [ "pytest-cov>=5.0", ] fitsio = ["fitsio"] -dev = ["shapepipe[doc,jupyter,lint,release,test,fitsio]"] +dev = ["shapepipe[doc,jupyter,lint,plot,release,test,fitsio]"] [project.scripts] shapepipe_run = "shapepipe.shapepipe_run:main" @@ -81,6 +88,11 @@ summary_run = "shapepipe.summary_run:main" canfar_submit_job = "shapepipe.canfar_run:run_job" canfar_monitor = "shapepipe.canfar_run:run_log" canfar_monitor_log = "shapepipe.canfar_run:run_monitor_log" +get_ccds_with_psf = "shapepipe.get_ccds_run:run_ccd_psf_handler" +download_headers = "shapepipe.coverage_run:run_download_headers" +extract_field_corners = "shapepipe.coverage_run:run_extract_corners" +build_coverage_map = "shapepipe.coverage_run:run_build_coverage" +plot_coverage_map = "shapepipe.coverage_run:run_plot_coverage" [tool.uv] # shapepipe targets Linux only; skip Windows/macOS wheel resolution diff --git a/scripts/python/get_ccds_with_psf.py b/scripts/python/get_ccds_with_psf.py old mode 100644 new mode 100755 index 5f917ad02..e284e2640 --- a/scripts/python/get_ccds_with_psf.py +++ b/scripts/python/get_ccds_with_psf.py @@ -9,192 +9,50 @@ """ - import sys -import numpy as np - -from shapepipe.utilities import summary - - -def get_lines(fname): - """Get Lines. - - Return list of lines read from a text file. - - Parameters - ---------- - fname: str - input file name - Returns: - list - IDs +from shapepipe.utilities.ccd_psf_handler import CcdPsfHandler - """ - IDs = [] - with open(fname) as f: - lines = f.readlines() - for line in lines: - IDs.append(line.rstrip()) - - return IDs +def run_ccd_psf_handler(args=None): + """Run CCD PSF Handler. -def get_exp_shdu_missing(patches): - """Get Exp Shdu Missing. - - Returns set of missing CCDs (single-exposure single-HDU IDs) from a list of patches. + Create instance and run the CCD PSF handler. Parameters ---------- - patches: list - input patches + args : list, optional + command line arguments Returns ------- - set - missing CCD IDs + int + exit code """ - exp_shdu_missing_all = set() - - for patch in patches: - - path_exp_shdu_missing = f"{patch}/summary/missing_job_32_all.txt" - exp_shdu_missing = get_lines(path_exp_shdu_missing) - - print(f"Patch {patch}: Found {len(exp_shdu_missing)} missing ccds", end="; ") - - exp_shdu_missing_all.update(exp_shdu_missing) - - print(f"cumulative {len(exp_shdu_missing_all)} missing ccds") - - print() - - return exp_shdu_missing_all - - -def get_exp(patches): - """Get Exp. - - Return set of exposures from a list of patches. - - Parameters - ---------- - patches: list - input patches - - Returns - ------- - set - exposure IDs - - """ - exp_all = set() - - for patch in patches: - - path_exp = f"{patch}/exp_numbers.txt" - exp = get_lines(path_exp) - - print(f"Patch {patch}: Found {len(exp)} exposures", end="; ") - - exp_all.update(exp) - - print(f"cumulative {len(exp_all)} exposures") + # Create instance + obj = CcdPsfHandler() - print() + return obj.run(args=args) - return exp_all +def main(argv=None): + """Main. -def get_ccds_with_psf(patches, n_CCD=40): - """Get CCDs With PSF. - - Return set of CCDs from list of patches. + Main program. Parameters ---------- - patches: list - input patches + argv : list, optional + command line arguments Returns ------- - set - CCD IDs + int + exit code """ - # Get missing CCDs - print("=== get missing CCDs ===") - exp_shdu_missing_all = get_exp_shdu_missing(patches) - - # Get all exposures used in tiles - print("=== get exposures ===") - exp_all = get_exp(patches) - - # Turn exposures into exposure-single-HDU names (CCDs) - exp_shdu_all = summary.get_all_shdus(exp_all, n_CCD) - - print(f"Found {len(exp_shdu_all)} CCDs") - - return exp_shdu_all - - -def get_ccds_with_psf_method_2(patches, n_CCD=40): - - for patch in patches: - directory = f"{patch}/exp_runs" - - for entry in os.scandir(directory): - pass - -def save(IDs, path): - """Save. - - Save list of IDs to text file. - - Parameters - ---------- - IDs: set - input IDs - - path: str - output file name - - """ - with open(path, "w") as f_out: - for ID in IDs: - print(ID, file=f_out) - -def main(argv): - """Main. - - Main program. - - """ - version = "v1.5" - - if version == "v1.4": - n_patch = 7 - elif version == "v1.5": - n_patch = 8 - else: - raise ValueError(f"Invalid version {version}") - - patches = [f'P{x}' for x in np.arange(n_patch) + 1] - - print(f"=== get_ccds_with_psf for version {version}, patches {patches} ===") - - print("=== method 1: exp_list - missing === ") - exp_shdu_all = get_ccds_with_psf(patches) - - save(exp_shdu_all, f"ccds_with_psf_{version}.txt") - - #print("=== method 2: star cats === ") - #exp_shdu_all_method_2 = get_ccds_with_psf_method_2(patches) - #save(exp_shdu_all_method_2, f"ccds_with_psf_{version}_method_2.txt") - - return 0 + return run_ccd_psf_handler(args=argv) if __name__ == "__main__": diff --git a/scripts/sh/build_and_plot_coverage_maps.sh b/scripts/sh/build_and_plot_coverage_maps.sh new file mode 100755 index 000000000..b7ea9c8b2 --- /dev/null +++ b/scripts/sh/build_and_plot_coverage_maps.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +set -euo pipefail + +# Build and plot per-CCD coverage (nexp) maps for a range of catalogue +# versions. Each map counts, per sky pixel, the number of exposures with a +# valid PSF model. The per-version flow is: +# 0. get_ccds_with_psf -> ccds_with_psf_.txt (valid CCD IDs) +# 1. download_headers -> per-exposure header text files +# 2. extract_field_corners-> exp_ra_dec_.txt (per-CCD corners) +# 3. build_coverage_map -> coverage_.x.hsp +# 4. plot_coverage_map -> SGC / NGC plots +# Steps 0-2 are expected to have been run already on canfar (they need +# VOSpace access); this script rebuilds from the extracted per-CCD corners and +# plots. Uncomment the marked lines to run the full chain. + +# Configuration variables + +## Catalogue versions +VERSIONS=("v1.3" "v1.4" "v1.5" "v1.6") + +## Output directory of plots +OUTPUT_DIR="coverage_map_plots" + +# healsparse resolution. nside=131072 gives ~0.1" per pixel, chosen to +# match the UNIONS bit-mask resolution so coverage and mask align pixel- +# wise. CoverageMapBuilder defaults to nside=2048 for lighter-weight +# offline use; override here for the production build. +BUILD_NSIDE=131072 +BUILD_CHANNELS=128 + +# Common parameters +VERBOSE="-v" + +## Colorbar +PLOT_COLORBAR="-C" +PLOT_MIN=1 +PLOT_MAX=5 + +# Plot parameters +## SGC region +SGC_RA_MIN=-20 +SGC_RA_MAX=45 +SGC_DEC_MIN=18 +SGC_DEC_MAX=40 + +## NGC region +NGC_RA_MIN=110 +NGC_RA_MAX=270 +NGC_DEC_MIN=28 +NGC_DEC_MAX=90 + +# Create output directory if it doesn't exist +mkdir -p "${OUTPUT_DIR}" + +# Loop over versions +for VERSION in "${VERSIONS[@]}"; do + echo "Processing ${VERSION}..." + + # Define file paths + CCD_LIST="ccds_with_psf_${VERSION}.txt" + HEADER_DIR="headers_${VERSION}" + INPUT_CORNERS="exp_ra_dec_${VERSION}.txt" + COVERAGE_MAP="coverage_${VERSION}.x.hsp" + + # Step 0-2: build the per-CCD corner file (canfar / VOSpace only). + # Uncomment to run the full chain from scratch. + # get_ccds_with_psf -V "${VERSION}" -o "${CCD_LIST}" ${VERBOSE} + # download_headers -i "${CCD_LIST}" -o "${HEADER_DIR}" ${VERBOSE} + # extract_field_corners -i "${HEADER_DIR}" -l "${CCD_LIST}" \ + # -o "${INPUT_CORNERS}" ${VERBOSE} + + # Build coverage map + echo " Building coverage map from ${INPUT_CORNERS}..." + CMD="build_coverage_map -i ${INPUT_CORNERS} -o ${COVERAGE_MAP} -c ${BUILD_CHANNELS} -n ${BUILD_NSIDE} ${VERBOSE}" + echo "$CMD" + $CMD + + # Plot SGC region + echo " Plotting SGC region..." + CMD="plot_coverage_map -i ${COVERAGE_MAP} -o ${OUTPUT_DIR}/coverage_${VERSION}_SGC.png ${VERBOSE} ${PLOT_COLORBAR} -R ${SGC_RA_MIN} -r ${SGC_RA_MAX} -D ${SGC_DEC_MIN} -d ${SGC_DEC_MAX} -m ${PLOT_MIN} -M ${PLOT_MAX}" + echo "$CMD" + $CMD + + # Plot NGC region + echo " Plotting NGC region..." + CMD="plot_coverage_map -i ${COVERAGE_MAP} -o ${OUTPUT_DIR}/coverage_${VERSION}_NGC.png ${VERBOSE} ${PLOT_COLORBAR} -R ${NGC_RA_MIN} -r ${NGC_RA_MAX} -D ${NGC_DEC_MIN} -d ${NGC_DEC_MAX} -m ${PLOT_MIN} -M ${PLOT_MAX}" + echo "$CMD" + $CMD + + echo " Done with ${VERSION}" + echo +done + +echo "All versions processed successfully!" diff --git a/src/shapepipe/coverage_run.py b/src/shapepipe/coverage_run.py new file mode 100644 index 000000000..15aaa1a37 --- /dev/null +++ b/src/shapepipe/coverage_run.py @@ -0,0 +1,104 @@ +"""COVERAGE_RUN + +Call coverage processing classes. + +Author: Martin Kilbinger + +""" + +import sys + +from shapepipe.utilities.header_downloader import HeaderDownloader +from shapepipe.utilities.field_corners_extractor import FieldCornersExtractor +from shapepipe.utilities.coverage_map_builder import CoverageMapBuilder +from shapepipe.utilities.coverage_plotter import CoveragePlotter + + +def run_download_headers(args=None): + """Run Download Headers. + + Download FITS headers from VOSpace for exposures in a CCD list. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code + + """ + obj = HeaderDownloader() + return obj.run(args=args) + + +def run_extract_corners(args=None): + """Run Extract Corners. + + Extract per-CCD sky-footprint corner coordinates from FITS headers. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code + + """ + obj = FieldCornersExtractor() + return obj.run(args=args) + + +def run_build_coverage(args=None): + """Run Build Coverage. + + Build HealSparse coverage maps from per-CCD corner coordinates. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code + + """ + obj = CoverageMapBuilder() + return obj.run(args=args) + + +def run_plot_coverage(args=None): + """Run Plot Coverage. + + Plot HealSparse coverage map. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code + + """ + obj = CoveragePlotter() + return obj.run(args=args) + + +def main(argv=None): + """Main. + + Main program. + + """ + # Scripts to call coverage classes are created by pyproject.toml + return 0 diff --git a/src/shapepipe/get_ccds_run.py b/src/shapepipe/get_ccds_run.py new file mode 100644 index 000000000..fb18b0a74 --- /dev/null +++ b/src/shapepipe/get_ccds_run.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +"""GET_CCDS_WITH_PSF + +Obtain list of CCDs (single-exposure single-HDU files) for which valid PSF information +is available. This can serve to create a footprint coverage mask. + +Author: Martin Kilbinger + +""" + +import sys + +from shapepipe.utilities.ccd_psf_handler import CcdPsfHandler + + +def run_ccd_psf_handler(args=None): + """Run CCD PSF Handler. + + Create instance and run the CCD PSF handler. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code + + """ + # Create instance + + obj = CcdPsfHandler() + + return obj.run(args=args) + + +def main(argv=None): + """Main. + """ + # A scripts to call the ccd psf class is created by pyproject.toml + return 0 diff --git a/src/shapepipe/utilities/ccd_psf_handler.py b/src/shapepipe/utilities/ccd_psf_handler.py new file mode 100644 index 000000000..2d0dc90eb --- /dev/null +++ b/src/shapepipe/utilities/ccd_psf_handler.py @@ -0,0 +1,387 @@ +"""CCD_PSF_HANDLER + +Obtain list of CCDs (single-exposure single-HDU files) for which valid PSF +information is available. This can serve to create a footprint coverage mask. + +Author: Mike Hudson, Martin Kilbinger + +""" + +import glob +import os +import re +import sys + +import numpy as np + +from cs_util import args as cs_args +from cs_util import logging + +from shapepipe.utilities import summary + + +class CcdPsfHandler(object): + """CCD PSF Handler Class. + + Handles extraction of CCDs with valid PSF information from shapepipe + patches. + """ + + def __init__(self): + """Initialize the handler.""" + self.params_default() + + def params_default(self): + """Set default parameters and command line options.""" + + self._params = { + "version_cat": "v1.6", + "n_CCD": 40, + "output": None, + } + + self._short_options = { + "version_cat": "-V", + "n_CCD": "-n", + "output": "-o", + } + + self._types = { + "n_CCD": "int", + } + + self._help_strings = { + "version_cat": "catalogue major version, allowed are v1.3, v1.4, v1.5, v1.6; default is {}", + + "n_CCD": "number of CCDs per exposure; default is {}", + "output": "output file path; default is ccds_with_psf_.txt", + } + + def set_params_from_command_line(self, args): + """Set Params From Command line. + + Only use when calling using python from command line. + Does not work from ipython or jupyter. + + Parameters + ---------- + args : list + command line arguments + + """ + # Read command line options + options = cs_args.parse_options( + self._params, + self._short_options, + self._types, + self._help_strings, + args=args, + ) + self._params = options + + # Save calling command + logging.log_command(args) + + def update_params(self): + """Update parameters. + + Set derived parameters based on input parameters. + """ + # Determine number of patches based on version + version = self._params["version_cat"] + if version in ("v1.3", "v1.4"): + n_patch = 7 + elif version == "v1.5": + n_patch = 8 + elif version == "v1.6": + n_patch = 9 + else: + raise ValueError(f"Invalid version {version}") + + self._params["n_patch"] = n_patch + self._params["patches"] = [f"P{x}" for x in np.arange(n_patch) + 1] + + # Set output file if not specified + if self._params["output"] is None: + self._params["output"] = f"ccds_with_psf_{version}.txt" + + def check_params(self): + """Check parameters for validity.""" + # Add any parameter validation logic here + pass + + def get_lines(self, fname): + """Get Lines. + + Return list of lines read from a text file. + + Parameters + ---------- + fname : str + input file name + + Returns + ------- + list + IDs + + """ + IDs = [] + with open(fname) as f: + lines = f.readlines() + for line in lines: + IDs.append(line.rstrip()) + + return IDs + + def get_exp_shdu_missing(self, patches): + """Get Exp Shdu Missing. + + Returns set of missing CCDs (single-exposure single-HDU IDs) from a list of patches. + + Parameters + ---------- + patches : list + input patches + + Returns + ------- + set + missing CCD IDs + + """ + exp_shdu_missing_all = set() + + for patch in patches: + + path_exp_shdu_missing = f"{patch}/summary/missing_job_32_all.txt" + exp_shdu_missing = self.get_lines(path_exp_shdu_missing) + + print( + f"Patch {patch}: Found {len(exp_shdu_missing)} missing ccds", + end="; ", + ) + + exp_shdu_missing_all.update(exp_shdu_missing) + + print(f"cumulative {len(exp_shdu_missing_all)} missing ccds") + + print() + + return exp_shdu_missing_all + + def get_exp(self, patches): + """Get Exp. + + Return set of exposures from a list of patches. + + Parameters + ---------- + patches : list + input patches + + Returns + ------- + set + exposure IDs + + """ + exp_all = set() + + for patch in patches: + + path_exp = f"{patch}/exp_numbers.txt" + exp = self.get_lines(path_exp) + + print(f"Patch {patch}: Found {len(exp)} exposures", end="; ") + + exp_all.update(exp) + + print(f"cumulative {len(exp_all)} exposures") + + print() + + return exp_all + + def get_ccds_with_psf(self, patches, n_CCD=40): + """Get CCDs With PSF. + + Return set of CCDs with valid PSF from a list of patches. A CCD has a + valid PSF if it appears among the exposures' single-HDU IDs but not in + the per-patch missing-CCD lists. + + Parameters + ---------- + patches : list + input patches + n_CCD : int + number of CCDs per exposure + + Returns + ------- + set + CCD IDs with valid PSF + + """ + # Get missing CCDs + print("=== get missing CCDs ===") + exp_shdu_missing_all = self.get_exp_shdu_missing(patches) + + # Get all exposures used in tiles + print("=== get exposures ===") + exp_all = self.get_exp(patches) + + # Turn exposures into exposure-single-HDU names (CCDs) + exp_shdu_all = set(summary.get_all_shdus(exp_all, n_CCD)) + + # Subtract the CCDs whose PSF model is missing + exp_shdu_with_psf = exp_shdu_all - exp_shdu_missing_all + + print( + f"Found {len(exp_shdu_all)} CCDs, " + f"{len(exp_shdu_missing_all)} missing, " + f"{len(exp_shdu_with_psf)} with valid PSF" + ) + + return exp_shdu_with_psf + + def get_ccds_with_psf_method_v1_3(self, patches, n_CCD=40): + """Get CCDs With PSF Method v1.3. + + Return set of CCDs with valid PSF by scanning run directories. + Finds star_selection files and checks corresponding stat files + for valid (non-nan) FWHM values. + + Parameters + ---------- + patches : list + input patches + n_CCD : int + number of CCDs per exposure (unused, kept for API consistency) + + Returns + ------- + set + CCD IDs with valid PSF + + """ + exp_shdu_all = set() + + for patch in patches: + # Find all star_selection files + mask_pattern = ( + f"{patch}/output/run_sp_exp_SxSePs*/" + f"setools_runner/output/mask/star_selection-*.fits" + ) + mask_files = glob.glob(mask_pattern) + + print( + f"Patch {patch}: Found {len(mask_files)} star_selection FITS" + + " files" + ) + + for mask_file in mask_files: + # Extract exp_shdu from filename + basename = os.path.basename(mask_file) + match = re.match(r"star_selection-(.+)\.fits", basename) + if not match: + print(f"Warning: Non-matching file name {basename}") + continue + exp_shdu = match.group(1) + + # Get corresponding stat file path + # Replace mask dir with stat/stat dir in the path + stat_file = mask_file.replace( + "/mask/star_selection-", + "/stat/star_stat-", + ).replace(".fits", ".txt") + + #print("MKDEBUG ", exp_shdu, stat_file) + + # Check if stat file exists and has valid FWHM + if not os.path.exists(stat_file): + print("MKDEBUG stat file ", stat_file, " does not exist") + continue + + # Read stat file and check for nan in "Mean star fwhm selected" + has_nan = False + with open(stat_file) as f: + for line in f: + if "Mean star fwhm selected" in line: + if "nan" in line.lower(): + has_nan = True + break + + if not has_nan: + #print("MKDEBUG append", exp_shdu) + exp_shdu_all.add(exp_shdu) + + print(f"After patch {patch}: {len(exp_shdu_all)} CCDs with valid PSF") + + print(f"Found {len(exp_shdu_all)} CCDs with valid PSF") + + return exp_shdu_all + + def save(self, IDs, path): + """Save. + + Save list of IDs to text file. + + Parameters + ---------- + IDs : set + input IDs + path : str + output file name + + """ + with open(path, "w") as f_out: + for ID in IDs: + print(ID, file=f_out) + + def run(self, args=None): + """Run. + + Main execution method. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code (0 for success) + + """ + if args is None: + args = sys.argv[1:] + + # Set parameters from command line + self.set_params_from_command_line(args) + self.update_params() + self.check_params() + + # Get parameters + patches = self._params["patches"] + version = self._params["version_cat"] + n_CCD = self._params["n_CCD"] + output = self._params["output"] + + print( + f"=== get_ccds_with_psf for version {version}, patches {patches} ===" + ) + + if self._params["version_cat"] == "v1.3": + print("=== method for 1.3: exp with PSF star sample === ") + exp_shdu_all = self.get_ccds_with_psf_method_v1_3(patches, n_CCD) + else: + print("=== method for >= 1.4: exp_list - missing === ") + exp_shdu_all = self.get_ccds_with_psf(patches, n_CCD) + + self.save(exp_shdu_all, output) + + print(f"Results saved to {output}") + + return 0 diff --git a/src/shapepipe/utilities/coverage_map_builder.py b/src/shapepipe/utilities/coverage_map_builder.py new file mode 100644 index 000000000..c7144ef5c --- /dev/null +++ b/src/shapepipe/utilities/coverage_map_builder.py @@ -0,0 +1,372 @@ +"""COVERAGE_MAP_BUILDER + +Build HealSparse coverage maps from per-CCD corner coordinates. + +Each input row is one CCD footprint. Since the CCDs of a single exposure do +not overlap, stamping value 1 per CCD polygon and accumulating makes the map +count, per sky pixel, the number of exposures with a valid PSF model covering +that pixel. + +Author: Mike Hudson, Martin Kilbinger + +""" + +import sys +from os.path import exists + +import numpy as np +import healsparse as hsp +import hpgeom as hpg + +from cs_util import args as cs_args +from cs_util import logging + +# Declination beyond which a polygon is considered too close to a pole for +# HealSparse's planar polygon fill to be reliable. CCD footprints are ~10 +# arcmin, so this is a guard against pathological headers, not a common path. +_DEC_POLE_LIMIT = 89.0 + + +def unwrap_ra(ra): + """Unwrap RA. + + Shift RA values onto a common branch when a polygon straddles the RA=0 + seam, so the corners describe the small footprint rather than its ~360° + complement. Values above 180 deg are shifted by -360 deg when the raw + spread exceeds 180 deg; otherwise the values are returned unchanged. + + Parameters + ---------- + ra : array_like + polygon RA corners, in degrees + + Returns + ------- + numpy.ndarray + RA corners on a common branch, in degrees + + """ + ra = np.asarray(ra, dtype=float) + if ra.max() - ra.min() > 180.0: + return np.where(ra > 180.0, ra - 360.0, ra) + return ra + + + +class CoverageMapBuilder(object): + """Coverage Map Builder Class. + + Builds HealSparse coverage maps from field corner coordinates. + """ + + def __init__(self): + """Initialize the builder.""" + self.params_default() + + def params_default(self): + """Set default parameters and command line options.""" + + self._params = { + "input_file": "exp_ra_dec.txt", + "output_file": "coverage.hsp", + "nside_coverage": 32, + "nside": 2048, + "apply_median_filter": False, + "n_median_iterations": 2, + "create_boolean": False, + "boolean_threshold": 3, + "boolean_output": None, + "create_plot": False, + "plot_output": None, + "plot_region": None, + "verbose": False, + } + + self._short_options = { + "input_file": "-i", + "output_file": "-o", + "nside_coverage": "-c", + "nside": "-n", + "apply_median_filter": "-m", + "n_median_iterations": "-N", + "create_boolean": "-b", + "boolean_threshold": "-t", + "boolean_output": "-B", + "create_plot": "-p", + "plot_output": "-P", + "plot_region": "-g", + } + + self._types = { + "nside_coverage": "int", + "nside": "int", + "apply_median_filter": "bool", + "n_median_iterations": "int", + "create_boolean": "bool", + "boolean_threshold": "int", + "create_plot": "bool", + "verbose": "bool", + } + + self._help_strings = { + "input_file": "input file with field corners; default is {}", + "output_file": "output HealSparse map file; default is {}", + "nside_coverage": "HealSparse coverage nside; default is {}", + "nside": "HealSparse map nside; default is {}", + "apply_median_filter": "apply median filter to smooth map; default is {}", + "n_median_iterations": "number of median filter iterations; default is {}", + "create_boolean": "create boolean coverage map; default is {}", + "boolean_threshold": "threshold value for boolean map; default is {}", + "boolean_output": "output file for boolean map; default is _bool.hsp", + "create_plot": "create plot of coverage map; default is {}", + "plot_output": "output file for plot; default is .png", + "plot_region": "predefined region for plot (NGC, SGC, fullsky); default is {}", + } + + def set_params_from_command_line(self, args): + """Set Params From Command line. + + Only use when calling using python from command line. + Does not work from ipython or jupyter. + + Parameters + ---------- + args : list + command line arguments + + """ + # Read command line options + options = cs_args.parse_options( + self._params, + self._short_options, + self._types, + self._help_strings, + args=args, + ) + self._params = options + + # Save calling command + logging.log_command(args) + + def update_params(self): + """Update parameters. + + Set derived parameters based on input parameters. + """ + # Set boolean output filename if not specified + if self._params["boolean_output"] is None: + output_file = self._params["output_file"] + if output_file.endswith(".hsp"): + base = output_file[:-4] + else: + base = output_file + self._params["boolean_output"] = f"{base}_bool.hsp" + + # Set plot output filename if not specified + if self._params["plot_output"] is None: + output_file = self._params["output_file"] + if output_file.endswith(".hsp"): + base = output_file[:-4] + else: + base = output_file + self._params["plot_output"] = f"{base}.png" + + def check_params(self): + """Check parameters for validity.""" + if not exists(self._params["input_file"]): + raise FileNotFoundError( + f"Input file not found: {self._params['input_file']}" + ) + + # Check nside values are powers of 2 + nside_coverage = self._params["nside_coverage"] + nside = self._params["nside"] + + if not (nside_coverage & (nside_coverage - 1) == 0): + raise ValueError( + f"nside_coverage must be a power of 2, got {nside_coverage}" + ) + + if not (nside & (nside - 1) == 0): + raise ValueError(f"nside must be a power of 2, got {nside}") + + def median_filter(self, hsp_map): + """Median Filter. + + Apply median filter to HealSparse map using neighbors. + + Parameters + ---------- + hsp_map : healsparse.HealSparseMap + input map + + Returns + ------- + healsparse.HealSparseMap + filtered map + + """ + nside = hsp_map.nside_sparse + + new_hsp = hsp_map.copy() + pixs = hsp_map.valid_pixels + n = hpg.neighbors(nside, pixs) + n = np.where(n >= 0, n, 0) + pix2 = pixs.reshape(len(pixs), 1) + n = np.hstack([n, pix2]) + mn = hsp_map[n] + new = np.median(mn, axis=1).astype(np.uint16) + new_hsp[pixs] = new + + return new_hsp + + def run(self, args=None): + """Run. + + Main execution method. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code (0 for success) + + """ + if args is None: + args = sys.argv[1:] + + # Set parameters from command line + self.set_params_from_command_line(args) + self.update_params() + self.check_params() + + # Get parameters + input_file = self._params["input_file"] + output_file = self._params["output_file"] + nside_coverage = self._params["nside_coverage"] + nside = self._params["nside"] + apply_median_filter = self._params["apply_median_filter"] + n_median_iterations = self._params["n_median_iterations"] + create_boolean = self._params["create_boolean"] + boolean_threshold = self._params["boolean_threshold"] + boolean_output = self._params["boolean_output"] + create_plot = self._params["create_plot"] + plot_output = self._params["plot_output"] + plot_region = self._params["plot_region"] + verbose = self._params["verbose"] + + if verbose: + print(f"Reading per-CCD corners from: {input_file}") + + # Load per-CCD corner data. Column 0 is a "-" string + # ID; the remaining 8 columns are the 4 RA then 4 Dec corners. + ccd_ids = np.atleast_1d(np.loadtxt(input_file, usecols=(0), dtype=str)) + corners = np.atleast_2d( + np.loadtxt(input_file, usecols=range(1, 9), dtype=float) + ) + ra_all = corners[:, :4] + dec_all = corners[:, 4:] + + print(f"Loaded {len(ccd_ids)} CCD footprints") + + if verbose: + print( + f"Creating HealSparse map (nside_coverage={nside_coverage}, nside={nside})" + ) + + # Create empty map + m = hsp.HealSparseMap.make_empty(nside_coverage, nside, np.uint16) + + # Add one polygon per CCD footprint + if verbose: + print("Adding polygons to map") + + n_added = 0 + n_skipped = 0 + for i in range(len(ccd_ids)): + dec = dec_all[i] + + # Pole guard: HealSparse's planar polygon fill degrades near the + # poles. CCD footprints never reach here, so warn and skip. + if np.any(np.abs(dec) >= _DEC_POLE_LIMIT): + print( + f"Warning: skipping CCD {ccd_ids[i]} with |dec| >= " + f"{_DEC_POLE_LIMIT} (too close to a pole)" + ) + n_skipped += 1 + continue + + # RA-wrap guard: put corners on a common branch across the seam. + ra = unwrap_ra(ra_all[i]) + + m += hsp.Polygon(ra=list(ra), dec=list(dec), value=1) + n_added += 1 + + if verbose and i % 1000 == 0: + print(f"{i:6d} / {len(ccd_ids):6d}") + + print(f"Added {n_added} polygons to map") + if n_skipped > 0: + print(f"Skipped {n_skipped} polygons near a pole") + + # Apply median filter if requested + if apply_median_filter: + if verbose: + print( + f"Applying median filter ({n_median_iterations} iterations)" + ) + + for i in range(n_median_iterations): + m = self.median_filter(m) + if verbose: + print(f" Iteration {i+1}/{n_median_iterations} complete") + + # Write main coverage map + if verbose: + print(f"Writing coverage map to: {output_file}") + + m.write(output_file, clobber=True) + print(f"Coverage map saved to {output_file}") + + # Create and write boolean map if requested + if create_boolean: + if verbose: + print( + f"Creating boolean map (threshold={boolean_threshold})" + ) + + c = hsp.HealSparseMap.make_empty(nside_coverage, nside, bool) + c[m.valid_pixels] = m[m.valid_pixels] >= boolean_threshold + + if verbose: + print(f"Writing boolean map to: {boolean_output}") + + c.write(boolean_output, clobber=True) + print(f"Boolean map saved to {boolean_output}") + + # Create plot if requested + if create_plot: + if verbose: + print(f"Creating plot of coverage map") + + try: + from shapepipe.utilities.coverage_plotter import CoveragePlotter + plotter = CoveragePlotter() + plotter.plot_coverage_map( + m, + plot_output, + region=plot_region, + vmax=boolean_threshold if create_boolean else 3, + colorbar=True, + colorbar_label="Coverage depth", + ) + print(f"Plot saved to {plot_output}") + except ImportError as e: + print(f"Warning: Could not create plot: {e}") + print("Install the cs_util package for plotting support.") + + return 0 diff --git a/src/shapepipe/utilities/coverage_plotter.py b/src/shapepipe/utilities/coverage_plotter.py new file mode 100644 index 000000000..7d837c2aa --- /dev/null +++ b/src/shapepipe/utilities/coverage_plotter.py @@ -0,0 +1,348 @@ +"""COVERAGE_PLOTTER + +Plot HealSparse coverage maps. + +Author: Mike Hudson, Martin Kilbinger + +""" + +import sys +from os.path import exists + +import numpy as np +import healsparse as hsp +import matplotlib +import matplotlib.pyplot as plt + +try: + from cs_util.plots import FootprintPlotter + HAS_FOOTPRINT_PLOTTER = True +except ImportError: + HAS_FOOTPRINT_PLOTTER = False + +from cs_util import args as cs_args +from cs_util import logging + + +class CoveragePlotter(object): + """Coverage Plotter Class. + + Plots HealSparse coverage maps. + """ + + def __init__(self): + """Initialize the plotter.""" + self.params_default() + + def params_default(self): + """Set default parameters and command line options.""" + + self._params = { + "input_file": None, + "output_file": "coverage_plot.png", + "region": None, + "ralo": 100.0, + "rahi": 270.0, + "declo": 28.0, + "dechi": 85.0, + "vmin": 0, + "vmax": 3, + "cmap": "rainbow", + "draw_milky_way": True, + "colorbar": True, + "colorbar_label": "Coverage depth", + "figsize_x": 12, + "figsize_y": 8, + "dpi": 200, + "verbose": False, + } + + self._short_options = { + "input_file": "-i", + "output_file": "-o", + "region": "-g", + "ralo": "-R", + "rahi": "-r", + "declo": "-D", + "dechi": "-d", + "vmin": "-m", + "vmax": "-M", + "cmap": "-c", + "draw_milky_way": "-w", + "colorbar": "-C", + "colorbar_label": "-L", + "figsize_x": "-x", + "figsize_y": "-y", + "dpi": "-p", + } + + self._types = { + "ralo": "float", + "rahi": "float", + "declo": "float", + "dechi": "float", + "vmin": "int", + "vmax": "int", + "draw_milky_way": "bool", + "colorbar": "bool", + "figsize_x": "float", + "figsize_y": "float", + "dpi": "int", + "verbose": "bool", + } + + self._help_strings = { + "input_file": "input HealSparse map file; required", + "output_file": "output plot file (png, pdf, etc.); default is {}", + "region": "predefined region (NGC, SGC, fullsky); overrides RA/Dec limits if specified; default is {}", + "ralo": "minimum RA for plot extent; default is {}", + "rahi": "maximum RA for plot extent; default is {}", + "declo": "minimum Dec for plot extent; default is {}", + "dechi": "maximum Dec for plot extent; default is {}", + "vmin": "minimum value for colormap; default is {}", + "vmax": "maximum value for colormap; default is {}", + "cmap": "matplotlib colormap name; default is {}", + "draw_milky_way": "draw Milky Way outline; default is {}", + "colorbar": "add colorbar to plot; default is {}", + "colorbar_label": "colorbar label; default is {}", + "figsize_x": "figure width in inches; default is {}", + "figsize_y": "figure height in inches; default is {}", + "dpi": "figure DPI; default is {}", + } + + def set_params_from_command_line(self, args): + """Set Params From Command line. + + Only use when calling using python from command line. + Does not work from ipython or jupyter. + + Parameters + ---------- + args : list + command line arguments + + """ + # Read command line options + options = cs_args.parse_options( + self._params, + self._short_options, + self._types, + self._help_strings, + args=args, + ) + self._params = options + + # Save calling command + logging.log_command(args) + + def update_params(self): + """Update parameters. + + Set derived parameters based on input parameters. + """ + pass + + def check_params(self): + """Check parameters for validity.""" + if self._params["input_file"] is None: + raise ValueError("Input file is required (use -i or --input_file)") + + if not exists(self._params["input_file"]): + raise FileNotFoundError( + f"Input file not found: {self._params['input_file']}" + ) + + if not HAS_FOOTPRINT_PLOTTER: + raise ImportError( + "cs_util package with FootprintPlotter is required for plotting. " + "Install with: pip install -e /path/to/cs_util" + ) + + # Check region validity. Reaches into FootprintPlotter._regions + # because cs_util doesn't expose a public region registry yet; + # update when it does. + region = self._params["region"] + if region is not None: + valid_regions = list(FootprintPlotter._regions.keys()) + if region not in valid_regions: + raise ValueError( + f"Invalid region '{region}'. Valid regions are: {', '.join(valid_regions)}" + ) + + def plot_coverage_map( + self, + hsp_map, + output_file, + region=None, + ralo=100.0, + rahi=270.0, + declo=28.0, + dechi=85.0, + vmin=0, + vmax=3, + title=None, + colorbar=True, + colorbar_label="Coverage depth", + figsize=(10, 10), + dpi=200, + ): + """Plot Coverage Map. + + Create a plot of the HealSparse coverage map using FootprintPlotter. + + Parameters + ---------- + hsp_map : healsparse.HealSparseMap + coverage map to plot + output_file : str + output file path + region : str, optional + predefined region name (NGC, SGC, fullsky) + ralo : float + minimum RA for plot extent (used if region is None) + rahi : float + maximum RA for plot extent (used if region is None) + declo : float + minimum Dec for plot extent (used if region is None) + dechi : float + maximum Dec for plot extent (used if region is None) + vmin : int + minimum value for colormap + vmax : int + maximum value for colormap + title : str, optional + plot title + colorbar : bool + add colorbar to plot + colorbar_label : str + colorbar label + figsize : tuple + figure size (width, height) in inches + dpi : int + figure DPI + + """ + # Set up matplotlib parameters + font = {"size": 16} + matplotlib.rc("font", **font) + matplotlib.rcParams["savefig.dpi"] = dpi + matplotlib.rcParams["figure.dpi"] = dpi + matplotlib.rcParams["figure.figsize"] = figsize + + # Create FootprintPlotter instance + plotter = FootprintPlotter( + nside_coverage=hsp_map.nside_coverage, + nside_map=hsp_map.nside_sparse + ) + + # Plot based on region or manual extent + if region is not None: + # Use predefined region + region_params = FootprintPlotter._regions[region] + plotter.plot_region( + hsp_map, + region_params, + outpath=output_file, + title=title, + colorbar=colorbar, + colorbar_label=colorbar_label + ) + else: + # Use manual RA/Dec limits + # Calculate ra_0 for projection + if ralo < rahi: + ra_0 = (ralo + rahi) / 2.0 + else: + ra_0 = 130.0 + + extend = [ralo, rahi, declo, dechi] + plotter.plot_area( + hsp_map, + ra_0=ra_0, + extend=extend, + vmin=vmin, + vmax=vmax, + outpath=output_file, + title=title, + colorbar=colorbar, + colorbar_label=colorbar_label + ) + + def run(self, args=None): + """Run. + + Main execution method. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code (0 for success) + + """ + if args is None: + args = sys.argv[1:] + + # Set parameters from command line + self.set_params_from_command_line(args) + self.update_params() + self.check_params() + + # Get parameters + input_file = self._params["input_file"] + output_file = self._params["output_file"] + region = self._params["region"] + ralo = self._params["ralo"] + rahi = self._params["rahi"] + declo = self._params["declo"] + dechi = self._params["dechi"] + vmin = self._params["vmin"] + vmax = self._params["vmax"] + colorbar = self._params["colorbar"] + colorbar_label = self._params["colorbar_label"] + figsize_x = self._params["figsize_x"] + figsize_y = self._params["figsize_y"] + dpi = self._params["dpi"] + verbose = self._params["verbose"] + + if verbose: + print(f"Reading coverage map from: {input_file}") + + # Load HealSparse map + hsp_map = hsp.HealSparseMap.read(input_file) + + print(f"Loaded map with nside={hsp_map.nside_sparse}") + print( + f"Valid pixels: {len(hsp_map.valid_pixels)} / {hsp_map.nside_sparse**2 * 12}" + ) + + if verbose: + if region: + print(f"Creating plot for region: {region}") + else: + print(f"Creating plot with extent: RA=[{ralo}, {rahi}], Dec=[{declo}, {dechi}]") + + # Create plot + self.plot_coverage_map( + hsp_map, + output_file, + region=region, + ralo=ralo, + rahi=rahi, + declo=declo, + dechi=dechi, + vmin=vmin, + vmax=vmax, + colorbar=colorbar, + colorbar_label=colorbar_label, + figsize=(figsize_x, figsize_y), + dpi=dpi, + ) + + print(f"Plot saved to {output_file}") + + return 0 diff --git a/src/shapepipe/utilities/field_corners_extractor.py b/src/shapepipe/utilities/field_corners_extractor.py new file mode 100644 index 000000000..7e3273456 --- /dev/null +++ b/src/shapepipe/utilities/field_corners_extractor.py @@ -0,0 +1,493 @@ +"""FIELD_CORNERS_EXTRACTOR + +Extract per-CCD sky-footprint corner coordinates from FITS headers. + +For each CCD (image HDU) in an exposure's multi-HDU header, the four corners +of the CCD are projected from pixel bounds to the sky, producing one row per +CCD keyed on its ``-`` ID. + +Author: Mike Hudson, Martin Kilbinger + +""" + +import sys +import os +import glob +import re +from os.path import exists +from multiprocessing import Pool, cpu_count + +import numpy as np +from astropy import wcs +from astropy.io.fits import Header + +from cs_util import args as cs_args +from cs_util import logging + +# Filename suffix convention for header files: <6+ digit exposure number>.txt +_EXPNUM_RE = re.compile(r'(\d+)\.txt$') + + +def _expnum_from_path(path): + """Extract exposure number from a header filename like ``1234567.txt``.""" + match = _EXPNUM_RE.search(path) + if match is None: + raise ValueError(f"Could not extract exposure number from {path!r}") + return int(match.group(1)) + + +def _image_shape(header): + """Return the CCD image ``(nx, ny)`` pixel shape from a header. + + For fpack tile-compressed HDUs (``ZIMAGE = T``), ``NAXIS1``/``NAXIS2`` + describe the compressed *binary table* (row width in bytes, row count), + not the image, and astropy's WCS never maps the true dimensions into + ``pixel_shape``. The true image size is carried by ``ZNAXIS1``/``ZNAXIS2``, + which are preferred here; a plain image HDU falls back to + ``NAXIS1``/``NAXIS2``. + + Parameters + ---------- + header : astropy.io.fits.Header + single-HDU header + + Returns + ------- + tuple + ``(nx, ny)`` image pixel dimensions + + Raises + ------ + ValueError + if neither the ``Z*`` nor the plain ``NAXIS`` dimensions are present + """ + if header.get("ZIMAGE", False): + keys = ("ZNAXIS1", "ZNAXIS2") + else: + keys = ("NAXIS1", "NAXIS2") + + if keys[0] not in header or keys[1] not in header: + raise ValueError( + f"Header is missing image dimensions ({keys[0]}/{keys[1]})" + ) + + return int(header[keys[0]]), int(header[keys[1]]) + + +def _parse_header_to_wcs(path): + """Parse a multi-HDU header text file into ``(wcs, (nx, ny))`` per HDU. + + The primary HDU is skipped. Each remaining HDU yields its WCS together with + the CCD image pixel shape (``ZNAXIS1/2`` for compressed HDUs, else + ``NAXIS1/2``); the shape is read from the header because the WCS drops the + ``Z*`` keywords. + """ + with open(path, "r") as f: + string = f.read() + tokens = re.split(r"^(END\s+)", string, flags=re.MULTILINE) + result = [] + for i in range(2, len(tokens) - 1, 2): + header = Header.fromstring(tokens[i] + tokens[i + 1], sep="\n") + result.append((wcs.WCS(header), _image_shape(header))) + return result + + +def _ccd_corners(w, shape): + """Return (ra, dec) of the 4 corners of a single CCD. + + The polygon is built from the CCD's pixel bounds ``shape = (nx, ny)`` using + pixel edges ``(-0.5, n - 0.5)`` so the quadrilateral covers the full CCD + area rather than pixel centres. Corners are returned in a consistent + counterclockwise pixel order (bottom-left, bottom-right, top-right, + top-left); since ``pixel_to_world`` maps this order to a simple, + non-self-intersecting sky quadrilateral for a CCD-sized field, and + HealSparse ``Polygon`` is orientation-agnostic, the resulting polygon is a + valid convex footprint. + + Parameters + ---------- + w : astropy.wcs.WCS + WCS of a single CCD + shape : tuple + ``(nx, ny)`` CCD image pixel dimensions + + Returns + ------- + tuple + ``(ra_list, dec_list)`` each of length 4, in degrees + """ + nx, ny = shape + + # Pixel-edge corners, counterclockwise: BL, BR, TR, TL + px = [-0.5, nx - 0.5, nx - 0.5, -0.5] + py = [-0.5, -0.5, ny - 0.5, ny - 0.5] + + sky = w.pixel_to_world(px, py) + return list(sky.ra.deg), list(sky.dec.deg) + + +class FieldCornersExtractor(object): + """Field Corners Extractor Class. + + Extracts RA/Dec coordinates of field corners from FITS headers. + """ + + def __init__(self): + """Initialize the extractor.""" + self.params_default() + + def params_default(self): + """Set default parameters and command line options.""" + + self._params = { + "input_dir": "header", + "output_file": "exp_ra_dec.txt", + "ccd_list": None, + "resume": False, + "n_processes": 1, + "verbose": False, + } + + self._short_options = { + "input_dir": "-i", + "output_file": "-o", + "ccd_list": "-l", + "resume": "-r", + "n_processes": "-n", + } + + self._types = { + "resume": "bool", + "n_processes": "int", + "verbose": "bool", + } + + self._help_strings = { + "input_dir": "input directory containing header files; default is {}", + "output_file": "output file for per-CCD corners; default is {}", + "ccd_list": "file of valid CCD IDs (output of get_ccds_with_psf); when given, only listed CCDs are written; on --resume, CCDs new to an expanded list are added without duplicating existing rows; default is all CCDs", + "resume": "resume from existing output file; default is {}", + "n_processes": f"number of parallel processes (1=serial, 0=auto={cpu_count()}); default is {{}}", + } + + def set_params_from_command_line(self, args): + """Set Params From Command line. + + Only use when calling using python from command line. + Does not work from ipython or jupyter. + + Parameters + ---------- + args : list + command line arguments + + """ + # Read command line options + options = cs_args.parse_options( + self._params, + self._short_options, + self._types, + self._help_strings, + args=args, + ) + self._params = options + + # Save calling command + logging.log_command(args) + + def update_params(self): + """Update parameters. + + Set derived parameters based on input parameters. + """ + # Ensure input directory ends without trailing slash + if self._params["input_dir"].endswith("/"): + self._params["input_dir"] = self._params["input_dir"][:-1] + + def check_params(self): + """Check parameters for validity.""" + if not exists(self._params["input_dir"]): + raise FileNotFoundError( + f"Input directory not found: {self._params['input_dir']}" + ) + + if self._params["ccd_list"] is not None and not exists( + self._params["ccd_list"] + ): + raise FileNotFoundError( + f"CCD list file not found: {self._params['ccd_list']}" + ) + + # Set n_processes to cpu_count if 0 + if self._params["n_processes"] == 0: + self._params["n_processes"] = cpu_count() + + if self._params["n_processes"] < 0: + raise ValueError( + f"n_processes must be >= 0, got {self._params['n_processes']}" + ) + + @staticmethod + def load_ccd_list(path): + """Load CCD List. + + Read valid CCD IDs (one ``-`` per line) into a set. + + Parameters + ---------- + path : str + path to the CCD list file + + Returns + ------- + set + valid CCD IDs + + """ + with open(path) as f: + return {line.strip() for line in f if line.strip()} + + @staticmethod + def process_single_header(args): + """Process Single Header. + + Worker function to process a single header file into per-CCD corners. + Static method so it can be pickled for multiprocessing. + + Parameters + ---------- + args : tuple + ``(path, verbose, valid_ccds)`` where ``path`` is the header file + path, ``verbose`` is a bool, and ``valid_ccds`` is a set of CCD IDs + to keep (or ``None`` to keep all) + + Returns + ------- + list or None + list of ``(ccd_id, ra_list, dec_list)`` for the exposure's CCDs on + success, ``None`` on failure + + """ + path, verbose, valid_ccds = args + expnum = _expnum_from_path(path) + + try: + wcs_shapes = _parse_header_to_wcs(path) + except Exception as e: + if verbose: + print(f"Failed to process {expnum}: {e}") + return None + + rows = [] + for ccd_idx, (w, shape) in enumerate(wcs_shapes): + ccd_id = f"{expnum}-{ccd_idx}" + if valid_ccds is not None and ccd_id not in valid_ccds: + continue + try: + ra, dec = _ccd_corners(w, shape) + except Exception as e: + if verbose: + print(f"Failed to process CCD {ccd_id}: {e}") + continue + rows.append((ccd_id, ra, dec)) + + return rows + + def get_done_ccds(self): + """Get Done CCDs. + + Read the set of CCD IDs already present in the output file. Resume is + keyed on individual CCD IDs, not exposure numbers: a CCD counts as done + only if its own row is present. This keeps resume correct when a write + was interrupted mid-exposure (the missing CCDs are filled in) and when + a rerun uses an expanded ``--ccd_list`` (the newly requested CCDs are + added), and it never duplicates a row. + + Returns + ------- + set + CCD IDs already written + + """ + output_file = self._params["output_file"] + + if not exists(output_file): + return set() + + try: + ids = np.atleast_1d( + np.loadtxt(output_file, usecols=(0), dtype=str) + ) + return set(ids.tolist()) + except Exception as e: + if self._params["verbose"]: + print(f"Could not read existing output file: {e}") + return set() + + def run(self, args=None): + """Run. + + Main execution method. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code (0 for success) + + """ + if args is None: + args = sys.argv[1:] + + # Set parameters from command line + self.set_params_from_command_line(args) + self.update_params() + self.check_params() + + # Get parameters + input_dir = self._params["input_dir"] + output_file = self._params["output_file"] + resume = self._params["resume"] + verbose = self._params["verbose"] + + # Load the set of valid CCD IDs to keep, if given + valid_ccds = None + if self._params["ccd_list"] is not None: + valid_ccds = self.load_ccd_list(self._params["ccd_list"]) + print(f"{len(valid_ccds)} valid CCDs in {self._params['ccd_list']}") + + # Find all header files + paths = glob.glob(f"{input_dir}/*.txt") + n = len(paths) + + if n == 0: + print(f"No header files found in {input_dir}/") + return 1 + + print(f"{n} header files found") + + # On resume, read the CCD IDs already written so we can skip them + # per-CCD (not per-exposure): a partially written exposure is completed + # rather than skipped, and no row is ever duplicated. + done_ccds = set() + if resume: + done_ccds = self.get_done_ccds() + print(f"{len(done_ccds)} CCDs already done") + + # Every header still needs parsing (a header may hold both done and + # not-yet-done CCDs); the per-CCD filter below decides what is written. + todo = paths + + # Get n_processes + n_processes = self._params["n_processes"] + + # Process headers + if n_processes == 1: + # Serial processing + results = self._process_serial(todo, verbose, valid_ccds) + else: + # Parallel processing + print(f"Using {n_processes} parallel processes") + results = self._process_parallel( + todo, n_processes, verbose, valid_ccds + ) + + # Flatten per-exposure CCD lists (dropping failed exposures), then drop + # CCDs already present in the output. + rows = [ + row + for res in results if res is not None + for row in res + if row[0] not in done_ccds + ] + + # Sort by exposure number, then CCD index + rows.sort(key=lambda r: (int(r[0].split("-")[0]), + int(r[0].split("-")[1]))) + + # Write results to file: " ra1 ra2 ra3 ra4 dec1 dec2 dec3 dec4" + mode = "a" if resume else "w" + with open(output_file, mode, buffering=1) as f: + for ccd_id, ra, dec in rows: + f.write(f"{ccd_id} ") + np.savetxt(f, ra, fmt="%9.5f", newline=" ") + np.savetxt(f, dec, fmt="%9.5f", newline=" ") + f.write("\n") + + n_exp_success = sum(1 for res in results if res is not None) + n_failed = len(todo) - n_exp_success + + print(f"Processed {n_exp_success} exposures, {len(rows)} new CCDs") + if n_failed > 0: + print(f"Failed to process {n_failed} exposures") + + print(f"Results written to {output_file}") + + return 0 + + def _process_serial(self, todo, verbose, valid_ccds): + """Process Serial. + + Process headers serially. + + Parameters + ---------- + todo : list + list of header file paths to process + verbose : bool + verbose output + valid_ccds : set or None + CCD IDs to keep, or ``None`` to keep all + + Returns + ------- + list + list of per-exposure CCD-row lists (or ``None`` for failures) + + """ + results = [] + n_todo = len(todo) + + for i, p in enumerate(todo): + result = self.process_single_header((p, verbose, valid_ccds)) + results.append(result) + + if verbose and i % 100 == 0: + print(f"{i:6d} / {n_todo:6d}") + + return results + + def _process_parallel(self, todo, n_processes, verbose, valid_ccds): + """Process Parallel. + + Process headers in parallel using multiprocessing. + + Parameters + ---------- + todo : list + list of header file paths to process + n_processes : int + number of parallel processes + verbose : bool + verbose output + valid_ccds : set or None + CCD IDs to keep, or ``None`` to keep all + + Returns + ------- + list + list of per-exposure CCD-row lists (or ``None`` for failures) + + """ + # Prepare arguments for worker function + args = [(p, verbose, valid_ccds) for p in todo] + + # Create pool and process + with Pool(processes=n_processes) as pool: + results = pool.map(self.process_single_header, args) + + return results diff --git a/src/shapepipe/utilities/header_downloader.py b/src/shapepipe/utilities/header_downloader.py new file mode 100644 index 000000000..122327a58 --- /dev/null +++ b/src/shapepipe/utilities/header_downloader.py @@ -0,0 +1,291 @@ +"""HEADER_DOWNLOADER + +Download FITS headers from VOSpace for exposures listed in a CCD file. + +Author: Mike Hudson, Martin Kilbinger + +""" + +import sys +import os +from os.path import exists + +import numpy as np +import vos + +from cs_util import args as cs_args +from cs_util import logging + + +class HeaderDownloader(object): + """Header Downloader Class. + + Downloads FITS headers from VOSpace for exposures in a CCD list. + """ + + def __init__(self): + """Initialize the downloader.""" + self.params_default() + + def params_default(self): + """Set default parameters and command line options.""" + + self._params = { + "input_file": None, + "output_dir": "header", + "vospace_path": "vos:cfis/pitcairn", # UNIONS/CFIS default; override for other surveys + "overwrite": False, + "dir_for_links": None, + "verbose": False, + } + + self._short_options = { + "input_file": "-i", + "output_dir": "-o", + "vospace_path": "-p", + "overwrite": "-O", + "dir_for_links": "-d", + } + + self._types = { + "overwrite": "bool", + "verbose": "bool", + } + + self._help_strings = { + "input_file": "input CCD list file (txt or csv); required", + "output_dir": "output directory for headers; default is {}", + "vospace_path": "VOSpace base path; default is {}", + "overwrite": "overwrite existing header files; default is {}", + "dir_for_links": "directory to check for existing headers to link instead of download; default is {}", + } + + def set_params_from_command_line(self, args): + """Set Params From Command line. + + Only use when calling using python from command line. + Does not work from ipython or jupyter. + + Parameters + ---------- + args : list + command line arguments + + """ + # Read command line options + options = cs_args.parse_options( + self._params, + self._short_options, + self._types, + self._help_strings, + args=args, + ) + self._params = options + + # Save calling command + logging.log_command(args) + + def update_params(self): + """Update parameters. + + Set derived parameters based on input parameters. + """ + # Ensure output directory ends without trailing slash + if self._params["output_dir"].endswith("/"): + self._params["output_dir"] = self._params["output_dir"][:-1] + + def check_params(self): + """Check parameters for validity.""" + if self._params["input_file"] is None: + raise ValueError("Input file is required (use -i or --input_file)") + + if not exists(self._params["input_file"]): + raise FileNotFoundError( + f"Input file not found: {self._params['input_file']}" + ) + + # Check if dir_for_links exists if specified + if self._params["dir_for_links"] is not None: + if not exists(self._params["dir_for_links"]): + raise FileNotFoundError( + f"Directory for links not found: {self._params['dir_for_links']}" + ) + if not os.path.isdir(self._params["dir_for_links"]): + raise ValueError( + f"Path is not a directory: {self._params['dir_for_links']}" + ) + + # Create output directory if it doesn't exist + if not exists(self._params["output_dir"]): + os.makedirs(self._params["output_dir"]) + if self._params["verbose"]: + print(f"Created output directory: {self._params['output_dir']}") + + def get_exposures(self, ccd_list_file): + """Get Exposures. + + Extract unique exposure numbers from CCD list file. + + Parameters + ---------- + ccd_list_file : str + path to CCD list file (txt or csv) + + Returns + ------- + np.array + unique exposure numbers + + """ + exps = [] + + # Check if CSV format + if ccd_list_file.endswith(".csv"): + import astropy.table + + t = astropy.table.Table.read(ccd_list_file) + expccd = t["CCD"].data + r = np.char.split(expccd, sep="-") + + for i, r1 in enumerate(r): + exp = int(r1[0]) + exps.append(exp) + else: + # Text format; atleast_1d so a single-line file (0-d array) is + # still iterable. + f = np.atleast_1d( + np.loadtxt(ccd_list_file, dtype="str", encoding="ascii") + ) + r = np.char.split(f, sep="-") + + for i, r1 in enumerate(r): + exp = int(r1[0]) + exps.append(exp) + + exps = np.array(exps) + uniq = np.unique(exps) + + return uniq + + def get_fits_header(self, expnum, client): + """Get FITS Header. + + Download FITS header from VOSpace, or create a symbolic link + if the header exists in dir_for_links. + + Parameters + ---------- + expnum : int + exposure number + client : vos.Client + VOSpace client + + Returns + ------- + bool + True if successful, False otherwise + + """ + vospace_path = self._params["vospace_path"] + output_dir = self._params["output_dir"] + overwrite = self._params["overwrite"] + dir_for_links = self._params["dir_for_links"] + + source = f"{vospace_path}/{expnum:d}p.fits.fz" + dest = f"{output_dir}/{expnum:d}.txt" + + if exists(dest) and not overwrite: + return True + + # Check if header exists in dir_for_links + if dir_for_links is not None: + link_source = os.path.abspath(f"{dir_for_links}/{expnum:d}.txt") + if exists(link_source): + try: + # Remove existing file/link if overwrite is True + if exists(dest): + os.remove(dest) + # Create symbolic link + os.symlink(link_source, dest) + return True + except Exception as e: + print(f"Could not create symlink from {link_source}: {e}") + # Fall through to download if symlink fails + + # Download from VOSpace atomically: copy to a temp file in the same + # directory, then rename on success. An interrupted transfer leaves + # only the temp file behind, so resume never treats a partial download + # as complete. + tmp_dest = f"{dest}.part" + try: + client.copy(source, tmp_dest, head=True) + os.rename(tmp_dest, dest) + return True + except Exception as e: + print(f"Could not copy {source}: {e}") + if exists(tmp_dest): + os.remove(tmp_dest) + return False + + def run(self, args=None): + """Run. + + Main execution method. + + Parameters + ---------- + args : list, optional + command line arguments + + Returns + ------- + int + exit code (0 for success) + + """ + if args is None: + args = sys.argv[1:] + + # Set parameters from command line + self.set_params_from_command_line(args) + self.update_params() + self.check_params() + + # Get parameters + input_file = self._params["input_file"] + output_dir = self._params["output_dir"] + verbose = self._params["verbose"] + + if verbose: + print(f"Reading CCD list from: {input_file}") + + # Extract unique exposures + exps = self.get_exposures(input_file) + + print(f"Found {len(exps)} unique exposures") + + if verbose: + print(f"Downloading headers to: {output_dir}/") + + # Initialize VOSpace client once + client = vos.Client() + + # Download headers + n_success = 0 + n_failed = 0 + + for i, exp in enumerate(exps): + success = self.get_fits_header(exp, client) + if success: + n_success += 1 + else: + n_failed += 1 + + if verbose and i % 100 == 0: + print(f"{i:6d} / {len(exps):6d}") + + print(f"Downloaded {n_success} headers") + if n_failed > 0: + print(f"Failed to download {n_failed} headers") + + return 0 diff --git a/tests/module/test_coverage.py b/tests/module/test_coverage.py new file mode 100644 index 000000000..ea48905c3 --- /dev/null +++ b/tests/module/test_coverage.py @@ -0,0 +1,674 @@ +"""UNIT / PROPERTY TESTS FOR THE COVERAGE-MASK FEATURE. + +Covers the pure and lightly-fixtured logic behind the per-CCD coverage nexp +masks: exposure-number parsing, the CCD-list -> unique-exposure reduction, the +handler's missing-CCD subtraction (pinned against the real ID format), per-CCD +corner extraction from plain and fpack-compressed multi-HDU headers, +``--ccd_list`` filtering, the per-CCD resume path, the builder's per-CCD row +parsing, the accumulated exposure-count (nexp) contract, the RA-wrap and pole +guards, the power-of-two ``nside`` validation, the atomic-download rename, and +the ``-h``/argv handling of the console runners. The heavy paths (real VOSpace +download, production-resolution map building, plotting, multiprocessing) are +exercised end to end by the pipeline, not here. +""" + +import sys +from types import SimpleNamespace + +import numpy as np +import numpy.testing as npt +import pytest +from astropy import wcs +from hypothesis import given +from hypothesis import strategies as st + +from shapepipe.utilities import summary +from shapepipe.utilities.ccd_psf_handler import CcdPsfHandler +from shapepipe.utilities.coverage_map_builder import ( + CoverageMapBuilder, + unwrap_ra, +) +from shapepipe.utilities.field_corners_extractor import ( + FieldCornersExtractor, + _ccd_corners, + _expnum_from_path, + _image_shape, + _parse_header_to_wcs, +) +from shapepipe.utilities.header_downloader import HeaderDownloader + + +def _tan_wcs(crval_ra, crval_dec=0.0, nx=2080, ny=4612): + """Build a minimal 2-D TAN WCS with a populated pixel shape. + + ``nx``/``ny`` set ``pixel_shape`` so ``_header_text`` can emit matching + ``NAXIS1``/``NAXIS2`` keywords. + """ + w = wcs.WCS(naxis=2) + w.wcs.ctype = ["RA---TAN", "DEC--TAN"] + w.wcs.crval = [crval_ra, crval_dec] + w.wcs.crpix = [nx / 2.0, ny / 2.0] + w.wcs.cd = [[-1e-5, 0.0], [0.0, 1e-5]] + w.pixel_shape = (nx, ny) + return w + + +def _header_text(wcs_list): + """Render a multi-HDU header text file: a primary HDU then one plain image + HDU per WCS, each carrying ``NAXIS1``/``NAXIS2``. + """ + blocks = ["SIMPLE = T"] + for w in wcs_list: + nx, ny = w.pixel_shape + header = w.to_header() + header["NAXIS"] = 2 + header["NAXIS1"] = nx + header["NAXIS2"] = ny + blocks.append("\n".join(str(card) for card in header.cards)) + return "".join(f"{block}\nEND \n" for block in blocks) + + +def _compressed_header_text(w): + """Render a one-CCD header mimicking an fpack tile-compressed HDU. + + ``NAXIS1``/``NAXIS2`` describe the compressed binary table (byte width, row + count); the true image dimensions live in ``ZNAXIS1``/``ZNAXIS2``. This is + the shape of headers fetched from 'p.fits.fz' with ``head=True``. + """ + nx, ny = w.pixel_shape + wcs_cards = "\n".join(str(card) for card in w.to_header().cards) + ccd = ( + "XTENSION= 'BINTABLE'\n" + "BITPIX = 8\n" + "NAXIS = 2\n" + "NAXIS1 = 8\n" + f"NAXIS2 = {ny}\n" + "PCOUNT = 1000000\n" + "GCOUNT = 1\n" + "TFIELDS = 1\n" + "ZIMAGE = T\n" + "ZBITPIX = -32\n" + "ZNAXIS = 2\n" + f"ZNAXIS1 = {nx}\n" + f"ZNAXIS2 = {ny}\n" + f"{wcs_cards}" + ) + return f"SIMPLE = T\nEND \n{ccd}\nEND \n" + + +# --------------------------------------------------------------------------- +# _expnum_from_path +# --------------------------------------------------------------------------- + +@pytest.mark.parametrize( + "path, expected", + [ + ("1234567.txt", 1234567), + ("/a/b/2143523.txt", 2143523), + ("headers/0000042.txt", 42), + ], +) +def test_expnum_from_path_extracts_trailing_number(path, expected): + """The trailing ``.txt`` is parsed as the exposure number.""" + assert _expnum_from_path(path) == expected + + +@pytest.mark.parametrize("path", ["no_number.txt", "1234567.fits", "abc.txt"]) +def test_expnum_from_path_raises_without_number(path): + """A filename without a trailing numeric stem raises ``ValueError``.""" + with pytest.raises(ValueError): + _expnum_from_path(path) + + +@given(st.integers(min_value=0, max_value=99999999)) +def test_expnum_from_path_roundtrips(expnum): + """Any exposure number round-trips through the filename convention.""" + assert _expnum_from_path(f"vos_headers/{expnum}.txt") == expnum + + +# --------------------------------------------------------------------------- +# HeaderDownloader.get_exposures +# --------------------------------------------------------------------------- + +def test_get_exposures_reduces_to_unique_exposures(tmp_path): + """A ``-`` CCD list collapses to its unique exposure numbers.""" + ccd_list = tmp_path / "ccds.txt" + ccd_list.write_text("2143523-0\n2143523-5\n2143524-3\n2143524-8\n") + + exps = HeaderDownloader().get_exposures(str(ccd_list)) + + npt.assert_array_equal(exps, np.array([2143523, 2143524])) + + +def test_get_exposures_single_line(tmp_path): + """A one-line CCD list (0-d loadtxt array) still yields one exposure.""" + ccd_list = tmp_path / "ccds.txt" + ccd_list.write_text("2143523-0\n") + + exps = HeaderDownloader().get_exposures(str(ccd_list)) + + npt.assert_array_equal(exps, np.array([2143523])) + + +def test_get_exposures_csv_matches_txt(tmp_path): + """The CSV and text code paths yield the same unique exposures.""" + txt = tmp_path / "ccds.txt" + txt.write_text("2143523-0\n2143523-5\n2143524-3\n") + csv = tmp_path / "ccds.csv" + csv.write_text("CCD\n2143523-0\n2143523-5\n2143524-3\n") + + dl = HeaderDownloader() + + npt.assert_array_equal( + dl.get_exposures(str(txt)), dl.get_exposures(str(csv)) + ) + + +# --------------------------------------------------------------------------- +# HeaderDownloader.get_fits_header — atomic rename +# --------------------------------------------------------------------------- + +def test_get_fits_header_writes_atomically(tmp_path): + """A successful download copies to ``.part`` then renames to the dest.""" + dl = HeaderDownloader() + dl._params["output_dir"] = str(tmp_path) + dl._params["overwrite"] = False + dl._params["dir_for_links"] = None + + dest = tmp_path / "42.txt" + tmp_dest = tmp_path / "42.txt.part" + + def fake_copy(source, target, head=True): + # The copy must land on the temp path, not the final destination. + assert target == str(tmp_dest) + with open(target, "w") as f: + f.write("HEADER") + + client = SimpleNamespace(copy=fake_copy) + + assert dl.get_fits_header(42, client) is True + assert dest.exists() + assert not tmp_dest.exists() + assert dest.read_text() == "HEADER" + + +def test_get_fits_header_failed_copy_leaves_no_dest(tmp_path): + """A failed download leaves no destination file (only, if any, ``.part``).""" + dl = HeaderDownloader() + dl._params["output_dir"] = str(tmp_path) + dl._params["overwrite"] = False + dl._params["dir_for_links"] = None + + def failing_copy(source, target, head=True): + raise RuntimeError("transfer interrupted") + + client = SimpleNamespace(copy=failing_copy) + + assert dl.get_fits_header(42, client) is False + assert not (tmp_path / "42.txt").exists() + assert not (tmp_path / "42.txt.part").exists() + + +# --------------------------------------------------------------------------- +# CcdPsfHandler.get_ccds_with_psf — missing-CCD subtraction +# --------------------------------------------------------------------------- + +def test_get_ccds_with_psf_subtracts_missing(monkeypatch): + """Valid CCDs are all exposure single-HDUs minus the missing set. + + The real ``summary.get_all_shdus`` is used so the cross-component + ``-`` ID format is pinned end to end. + """ + handler = CcdPsfHandler() + + # Two exposures, 3 CCDs each -> 6 candidate CCDs; two are missing. + monkeypatch.setattr(handler, "get_exp", lambda patches: {"100", "200"}) + monkeypatch.setattr( + handler, + "get_exp_shdu_missing", + lambda patches: {"100-1", "200-2"}, + ) + + result = handler.get_ccds_with_psf(["P1"], n_CCD=3) + + # get_all_shdus yields "-" for ccd in range(n_CCD). + assert result == {"100-0", "100-2", "200-0", "200-1"} + # Guard the assumption that the missing IDs share the produced format. + assert set(summary.get_all_shdus({"100"}, 3)) == {"100-0", "100-1", "100-2"} + + +@pytest.mark.parametrize( + ("version", "n_patch"), + [("v1.3", 7), ("v1.4", 7), ("v1.5", 8), ("v1.6", 9)], +) +def test_version_to_patch_count(version, n_patch): + """Each v1.x catalogue version maps to its patch count.""" + handler = CcdPsfHandler() + handler._params["version_cat"] = version + handler.update_params() + assert handler._params["n_patch"] == n_patch + assert len(handler._params["patches"]) == n_patch + + +def test_invalid_version_raises(): + """An unknown catalogue version fails loudly.""" + handler = CcdPsfHandler() + handler._params["version_cat"] = "v9.9" + with pytest.raises(ValueError, match="v9.9"): + handler.update_params() + + +# --------------------------------------------------------------------------- +# image-shape resolution (fpack ZNAXIS vs plain NAXIS) +# --------------------------------------------------------------------------- + +def test_image_shape_prefers_znaxis_for_compressed_header(tmp_path): + """A compressed HDU reports ZNAXIS dims, not the binary-table NAXIS.""" + w = _tan_wcs(100.0, 20.0, nx=2080, ny=4612) + path = tmp_path / "1234567.txt" + path.write_text(_compressed_header_text(w)) + + (parsed_w, shape), = _parse_header_to_wcs(str(path)) + + # WCS pixel_shape would wrongly report the compressed byte width (8). + assert parsed_w.pixel_shape == (8, 4612) + # _image_shape recovers the true image dimensions. + assert shape == (2080, 4612) + + +def test_image_shape_falls_back_to_naxis(tmp_path): + """A plain image HDU (no ZIMAGE) uses NAXIS1/NAXIS2.""" + w = _tan_wcs(100.0, 20.0, nx=2080, ny=4612) + path = tmp_path / "1234567.txt" + path.write_text(_header_text([w])) + + (_, shape), = _parse_header_to_wcs(str(path)) + + assert shape == (2080, 4612) + + +def test_image_shape_raises_without_dimensions(): + """A header with no image dimensions raises a clear ``ValueError``.""" + from astropy.io.fits import Header + + header = Header() + header["CTYPE1"] = "RA---TAN" + + with pytest.raises(ValueError, match="image dimensions"): + _image_shape(header) + + +def test_compressed_header_corners_are_full_width(tmp_path): + """Corners from a compressed header span the true CCD width, not 8 px.""" + w = _tan_wcs(100.0, 20.0, nx=2080, ny=4612) + path = tmp_path / "1234567.txt" + path.write_text(_compressed_header_text(w)) + + (parsed_w, shape), = _parse_header_to_wcs(str(path)) + ra, dec = _ccd_corners(parsed_w, shape) + + # RA extent must reflect ~2080 px * 1e-5 deg/px * cos(dec), not 8 px. + ra_extent = max(ra) - min(ra) + assert ra_extent > 0.01 + + +# --------------------------------------------------------------------------- +# per-CCD corner geometry +# --------------------------------------------------------------------------- + +def test_ccd_corners_returns_four_corners_around_centre(): + """The 4 corners bracket the CCD centre and cover its full pixel extent.""" + nx, ny = 2080, 4612 + w = _tan_wcs(100.0, 20.0, nx=nx, ny=ny) + + ra, dec = _ccd_corners(w, (nx, ny)) + + assert len(ra) == 4 + assert len(dec) == 4 + # The footprint straddles the CRVAL centre in both coordinates. + assert min(ra) < 100.0 < max(ra) + assert min(dec) < 20.0 < max(dec) + # Extent matches the CD-scaled pixel span (with edge padding). + npt.assert_allclose(max(dec) - min(dec), ny * 1e-5, rtol=1e-3) + + +def test_parse_header_to_wcs_returns_one_wcs_per_extension(tmp_path): + """One (wcs, shape) pair is returned per CCD HDU; the primary is skipped.""" + ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] + + path = tmp_path / "1234567.txt" + path.write_text(_header_text(ccd_wcs)) + + result = _parse_header_to_wcs(str(path)) + + assert len(result) == len(ccd_wcs) + assert all(shape == (2080, 4612) for _, shape in result) + + +# --------------------------------------------------------------------------- +# FieldCornersExtractor.process_single_header — per-CCD rows and filtering +# --------------------------------------------------------------------------- + +def test_process_single_header_emits_one_row_per_ccd(tmp_path): + """Without a CCD list, every HDU yields a ``-`` row.""" + ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] + path = tmp_path / "1234567.txt" + path.write_text(_header_text(ccd_wcs)) + + rows = FieldCornersExtractor.process_single_header( + (str(path), False, None) + ) + + assert [r[0] for r in rows] == [ + "1234567-0", + "1234567-1", + "1234567-2", + ] + assert all(len(r[1]) == 4 and len(r[2]) == 4 for r in rows) + + +def test_process_single_header_filters_to_ccd_list(tmp_path): + """A ``valid_ccds`` set keeps only the listed CCDs of the exposure.""" + ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] + path = tmp_path / "1234567.txt" + path.write_text(_header_text(ccd_wcs)) + + rows = FieldCornersExtractor.process_single_header( + (str(path), False, {"1234567-0", "1234567-2"}) + ) + + assert [r[0] for r in rows] == ["1234567-0", "1234567-2"] + + +def test_load_ccd_list_reads_ids(tmp_path): + """The CCD list loader returns the stripped, non-blank IDs as a set.""" + path = tmp_path / "ccds.txt" + path.write_text("100-0\n100-1\n\n200-5\n") + + assert FieldCornersExtractor.load_ccd_list(str(path)) == { + "100-0", + "100-1", + "200-5", + } + + +def test_run_extract_writes_per_ccd_rows(tmp_path): + """End to end: run() writes one per-CCD row filtered by the CCD list.""" + header_dir = tmp_path / "headers" + header_dir.mkdir() + ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] + (header_dir / "1234567.txt").write_text(_header_text(ccd_wcs)) + + ccd_list = tmp_path / "ccds.txt" + ccd_list.write_text("1234567-0\n1234567-2\n") + + out = tmp_path / "corners.txt" + + extractor = FieldCornersExtractor() + extractor.run( + args=[ + "-i", str(header_dir), + "-l", str(ccd_list), + "-o", str(out), + ] + ) + + lines = out.read_text().splitlines() + assert len(lines) == 2 + ids = [line.split()[0] for line in lines] + assert ids == ["1234567-0", "1234567-2"] + # Each row: 1 ID + 4 RA + 4 Dec = 9 columns. + assert all(len(line.split()) == 9 for line in lines) + + +# --------------------------------------------------------------------------- +# FieldCornersExtractor resume path (per-CCD done-set) +# --------------------------------------------------------------------------- + +def _write_headers(header_dir, expnums, n_ccd=3): + """Write one plain multi-HDU header per exposure into ``header_dir``.""" + header_dir.mkdir(exist_ok=True) + for j, expnum in enumerate(expnums): + ccd_wcs = [_tan_wcs(10.0 + j + i) for i in range(n_ccd)] + (header_dir / f"{expnum}.txt").write_text(_header_text(ccd_wcs)) + + +def test_get_done_ccds_reads_present_ids(tmp_path): + """The done-set is the exact set of CCD IDs already in the output.""" + out = tmp_path / "corners.txt" + out.write_text( + "1234567-0 10 10 10 10 20 20 20 20\n" + "1234567-2 10 10 10 10 20 20 20 20\n" + ) + + extractor = FieldCornersExtractor() + extractor._params["output_file"] = str(out) + + assert extractor.get_done_ccds() == {"1234567-0", "1234567-2"} + + +def test_resume_adds_new_exposure_without_duplicating(tmp_path): + """Resume appends a new exposure and leaves existing rows untouched.""" + header_dir = tmp_path / "headers" + _write_headers(header_dir, [1000001, 1000002]) + + out = tmp_path / "corners.txt" + # Pre-populate with exposure 1's three CCD rows. + pre = FieldCornersExtractor().process_single_header( + (str(header_dir / "1000001.txt"), False, None) + ) + with open(out, "w") as f: + for ccd_id, ra, dec in pre: + f.write(f"{ccd_id} " + " ".join(f"{v:.5f}" for v in ra + dec) + "\n") + + FieldCornersExtractor().run( + args=["-i", str(header_dir), "-o", str(out), "-r"] + ) + + ids = [line.split()[0] for line in out.read_text().splitlines()] + # No duplicate exposure-1 rows; exposure 2's three CCDs added. + assert ids.count("1000001-0") == 1 + assert sorted(ids) == [ + "1000001-0", "1000001-1", "1000001-2", + "1000002-0", "1000002-1", "1000002-2", + ] + + +def test_resume_completes_partial_exposure(tmp_path): + """An exposure interrupted mid-write is completed, not skipped.""" + header_dir = tmp_path / "headers" + _write_headers(header_dir, [1000001]) + + out = tmp_path / "corners.txt" + # Simulate an interrupt: only the first of exposure 1's CCDs was written. + pre = FieldCornersExtractor().process_single_header( + (str(header_dir / "1000001.txt"), False, None) + ) + ccd_id, ra, dec = pre[0] + with open(out, "w") as f: + f.write(f"{ccd_id} " + " ".join(f"{v:.5f}" for v in ra + dec) + "\n") + + FieldCornersExtractor().run( + args=["-i", str(header_dir), "-o", str(out), "-r"] + ) + + ids = [line.split()[0] for line in out.read_text().splitlines()] + # The missing CCDs are filled in; the present one is not duplicated. + assert sorted(ids) == ["1000001-0", "1000001-1", "1000001-2"] + assert ids.count("1000001-0") == 1 + + +# --------------------------------------------------------------------------- +# CoverageMapBuilder.check_params (nside validation) +# --------------------------------------------------------------------------- + +def test_check_params_accepts_power_of_two_nside(tmp_path): + """Powers of two for both nside values pass validation.""" + infile = tmp_path / "corners.txt" + infile.write_text("100-0 0.0 1.0 1.0 0.0 0.0 0.0 1.0 1.0\n") + + builder = CoverageMapBuilder() + builder._params["input_file"] = str(infile) + builder._params["nside_coverage"] = 32 + builder._params["nside"] = 2048 + + builder.check_params() + + +@pytest.mark.parametrize( + "nside_coverage, nside", [(33, 2048), (32, 100), (32, 3000)] +) +def test_check_params_rejects_non_power_of_two_nside( + tmp_path, nside_coverage, nside +): + """A non-power-of-two nside raises ``ValueError``.""" + infile = tmp_path / "corners.txt" + infile.write_text("100-0 0.0 1.0 1.0 0.0 0.0 0.0 1.0 1.0\n") + + builder = CoverageMapBuilder() + builder._params["input_file"] = str(infile) + builder._params["nside_coverage"] = nside_coverage + builder._params["nside"] = nside + + with pytest.raises(ValueError): + builder.check_params() + + +def test_check_params_missing_input_file_raises(tmp_path): + """A missing input file raises ``FileNotFoundError``.""" + builder = CoverageMapBuilder() + builder._params["input_file"] = str(tmp_path / "does_not_exist.txt") + + with pytest.raises(FileNotFoundError): + builder.check_params() + + +# --------------------------------------------------------------------------- +# CoverageMapBuilder — parsing, nexp contract, RA-wrap and pole guards +# --------------------------------------------------------------------------- + +def _read_map(path): + import healsparse as hsp + + return hsp.HealSparseMap.read(str(path)) + + +def test_build_map_single_row(tmp_path): + """A one-row corners file exercises the atleast_1d/2d parsing guards.""" + infile = tmp_path / "corners.txt" + infile.write_text("100-0 9.9 10.1 10.1 9.9 0.0 0.0 0.2 0.2\n") + out = tmp_path / "cov.hsp" + + CoverageMapBuilder().run( + args=["-i", str(infile), "-o", str(out), "-c", "32", "-n", "1024"] + ) + + m = _read_map(out) + assert 0 < len(m.valid_pixels) < 200 + assert m[m.valid_pixels].max() == 1 + + +def test_build_map_nexp_counts_overlapping_exposures(tmp_path): + """Two overlapping CCDs from different exposures give value 2 in overlap.""" + # Two 0.4x0.4 deg CCDs offset by 0.2 deg in RA -> a central overlap strip. + infile = tmp_path / "corners.txt" + infile.write_text( + "100-0 9.8 10.2 10.2 9.8 19.8 19.8 20.2 20.2\n" + "200-0 10.0 10.4 10.4 10.0 19.8 19.8 20.2 20.2\n" + ) + out = tmp_path / "cov.hsp" + + CoverageMapBuilder().run( + args=["-i", str(infile), "-o", str(out), "-c", "32", "-n", "1024"] + ) + + m = _read_map(out) + values = m[m.valid_pixels] + # The overlap is covered by both exposures (value 2); the union edges by + # one (value 1). Both must be present; nothing exceeds 2. + assert values.max() == 2 + assert values.min() == 1 + assert (values == 2).sum() > 0 + assert (values == 1).sum() > 0 + + +def test_unwrap_ra_shifts_straddling_seam(): + """A polygon straddling RA=0 is put on a common (negative) branch.""" + out = unwrap_ra([359.9, 0.1, 0.1, 359.9]) + npt.assert_allclose(sorted(out), [-0.1, -0.1, 0.1, 0.1], atol=1e-6) + + +def test_unwrap_ra_leaves_normal_polygon_unchanged(): + """A polygon well away from the seam is returned unchanged.""" + npt.assert_allclose( + unwrap_ra([99.9, 100.1, 100.1, 99.9]), + [99.9, 100.1, 100.1, 99.9], + ) + + +def test_build_map_invariant_under_ra_shift(tmp_path): + """A seam CCD's footprint matches an identical CCD shifted +10 deg in RA. + + Rotating both the seam CCD (via +360/unwrap) and a reference CCD onto the + same RA and comparing pixel counts fails if the seam polygon were filling + the ~360 deg complement. This is the real RA-wrap regression guard. + """ + # Seam CCD straddling RA=0, and the same CCD translated to RA~10. + seam = tmp_path / "seam.txt" + seam.write_text("100-0 359.9 0.1 0.1 359.9 20.0 20.0 20.2 20.2\n") + ref = tmp_path / "ref.txt" + ref.write_text("200-0 9.9 10.1 10.1 9.9 20.0 20.0 20.2 20.2\n") + + m_seam = tmp_path / "seam.hsp" + m_ref = tmp_path / "ref.hsp" + CoverageMapBuilder().run( + args=["-i", str(seam), "-o", str(m_seam), "-c", "32", "-n", "1024"] + ) + CoverageMapBuilder().run( + args=["-i", str(ref), "-o", str(m_ref), "-c", "32", "-n", "1024"] + ) + + n_seam = len(_read_map(m_seam).valid_pixels) + n_ref = len(_read_map(m_ref).valid_pixels) + + # Same-size footprints at the same declination: pixel counts agree to + # within a few boundary pixels, and are nowhere near a hemisphere. + assert abs(n_seam - n_ref) <= max(3, int(0.1 * n_ref)) + assert 0 < n_seam < 200 + + +def test_build_map_pole_guard_skips_polygon(tmp_path, capsys): + """A polygon with |dec| near 90 deg is skipped with a warning.""" + infile = tmp_path / "corners.txt" + infile.write_text( + "100-0 10.0 10.2 10.2 10.0 89.5 89.5 89.7 89.7\n" + "200-0 10.0 10.2 10.2 10.0 20.0 20.0 20.2 20.2\n" + ) + out = tmp_path / "cov.hsp" + + CoverageMapBuilder().run( + args=["-i", str(infile), "-o", str(out), "-c", "32", "-n", "1024"] + ) + + captured = capsys.readouterr() + assert "skipping CCD 100-0" in captured.out + assert "Added 1 polygons" in captured.out + + +# --------------------------------------------------------------------------- +# console-runner argv handling (regression guard for the -h entry points) +# --------------------------------------------------------------------------- + +def test_run_help_flag_exits_cleanly(monkeypatch): + """``run()`` with no args reads ``sys.argv[1:]`` so ``-h`` exits 0. + + Guards the regression where the runners parsed the full ``sys.argv`` + (including ``argv[0]``), which made ``-h`` collide with the integer + ``-c`` option and exit non-zero. + """ + monkeypatch.setattr(sys, "argv", ["extract_field_corners", "-h"]) + + with pytest.raises(SystemExit) as excinfo: + FieldCornersExtractor().run() + + assert excinfo.value.code == 0 diff --git a/uv.lock b/uv.lock index 19a25cf42..ca985ee4c 100644 --- a/uv.lock +++ b/uv.lock @@ -14,7 +14,7 @@ name = "accessible-pygments" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } wheels = [ @@ -62,8 +62,8 @@ name = "anyio" version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ @@ -75,7 +75,7 @@ name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argon2-cffi-bindings" }, + { name = "argon2-cffi-bindings", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ @@ -87,7 +87,7 @@ name = "argon2-cffi-bindings" version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" } wheels = [ @@ -127,8 +127,8 @@ name = "arrow" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil" }, - { name = "tzdata" }, + { name = "python-dateutil", marker = "sys_platform == 'linux'" }, + { name = "tzdata", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } wheels = [ @@ -140,11 +140,11 @@ name = "astropy" version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy-iers-data" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pyerfa" }, - { name = "pyyaml" }, + { name = "astropy-iers-data", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pyerfa", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/c4/21be4313ddfde5f60e0607fd307f367b9e0f0bf153a89b10cbd036dd8cfd/astropy-8.0.1.tar.gz", hash = "sha256:45ca31d5b91fa294cd590a4791a32db94de7f9c8a343155f4d5877baa82351da", size = 7152500, upload-time = "2026-07-05T07:24:48.482Z" } wheels = [ @@ -158,8 +158,8 @@ name = "astropy-healpix" version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "numpy" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/15/c1/aeb3fe3be2ee863708d625014267c71abfe20ddaa293b3d4ddb72ee1d6e9/astropy_healpix-2.0.1.tar.gz", hash = "sha256:0e3f1c94064c45da779900cb90c938df7aef99a924abb23eeb893b16540e77e6", size = 112256, upload-time = "2026-07-20T21:07:30.004Z" } wheels = [ @@ -185,13 +185,13 @@ name = "astroquery" version = "0.4.11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "beautifulsoup4" }, - { name = "html5lib" }, - { name = "keyring" }, - { name = "numpy" }, - { name = "pyvo" }, - { name = "requests" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "beautifulsoup4", marker = "sys_platform == 'linux'" }, + { name = "html5lib", marker = "sys_platform == 'linux'" }, + { name = "keyring", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pyvo", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/48/273dbde090e071f9d264d084bc49193d126498d2906172b78febd9d62e28/astroquery-0.4.11.tar.gz", hash = "sha256:5537529bddc7fa07e773d5cd9baca593e3f5d93474edd1914f68e89506042b33", size = 12561055, upload-time = "2025-09-20T04:26:36.744Z" } wheels = [ @@ -239,8 +239,8 @@ name = "beautifulsoup4" version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, + { name = "soupsieve", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ @@ -252,7 +252,7 @@ name = "bleach" version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "webencodings" }, + { name = "webencodings", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ @@ -261,7 +261,7 @@ wheels = [ [package.optional-dependencies] css = [ - { name = "tinycss2" }, + { name = "tinycss2", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -269,8 +269,8 @@ name = "build" version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging" }, - { name = "pyproject-hooks" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pyproject-hooks", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/b7/1db48a9ce2984842c8c886432ec8a2719613322e868a966ba82a28862f25/build-1.6.0.tar.gz", hash = "sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af", size = 113825, upload-time = "2026-08-27T21:01:16.458Z" } wheels = [ @@ -282,12 +282,12 @@ name = "cadcutils" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "distro" }, - { name = "lxml" }, - { name = "packaging" }, - { name = "pyopenssl" }, - { name = "requests" }, - { name = "setuptools" }, + { name = "distro", marker = "sys_platform == 'linux'" }, + { name = "lxml", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pyopenssl", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/44/4964823c72c9e6a93e26de0118cead37fa3c3600b4677f0c06788c903c80/cadcutils-1.6.2.tar.gz", hash = "sha256:6e2d7822756d48a363bc5f857cd717f80aab760554df86518b957f8826a7a906", size = 94673, upload-time = "2026-06-22T17:35:47.303Z" } wheels = [ @@ -299,10 +299,10 @@ name = "camb" version = "2.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "scipy" }, - { name = "sympy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/0b/856e8e329c088155509e3c1a999b02cd3f0333a31fc1fa37653455a4ec80/camb-2.0.4.tar.gz", hash = "sha256:2f1f5f3b3964a3746693dc88a54218dca4ea049a04726f8177664c11d2d2616e", size = 925860, upload-time = "2026-08-25T22:10:58.612Z" } wheels = [ @@ -315,19 +315,19 @@ name = "canfar" version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cadcutils" }, - { name = "click" }, - { name = "defusedxml" }, - { name = "httpx", extra = ["http2"] }, - { name = "humanize" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyyaml" }, - { name = "questionary" }, - { name = "rich" }, - { name = "segno" }, - { name = "toml" }, - { name = "typer" }, + { name = "cadcutils", marker = "sys_platform == 'linux'" }, + { name = "click", marker = "sys_platform == 'linux'" }, + { name = "defusedxml", marker = "sys_platform == 'linux'" }, + { name = "httpx", extra = ["http2"], marker = "sys_platform == 'linux'" }, + { name = "humanize", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "pydantic-settings", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "questionary", marker = "sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'linux'" }, + { name = "segno", marker = "sys_platform == 'linux'" }, + { name = "toml", marker = "sys_platform == 'linux'" }, + { name = "typer", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/d8/a457c57a8c83d6a6a4a96aa41fc46484489a83932bc5fe26180e1daef130/canfar-1.4.1.tar.gz", hash = "sha256:bbd7a37a21e6f9edf8790176797763187fe38a39758c21d652d77ad2fb76a3ee", size = 25743742, upload-time = "2026-06-11T20:39:05.07Z" } wheels = [ @@ -348,7 +348,7 @@ name = "cffi" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ @@ -538,7 +538,7 @@ name = "conda-inject" version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/a8/8dc86113c65c949cc72d651461d6e4c544b3302a85ed14a5298829e6a419/conda_inject-1.3.2.tar.gz", hash = "sha256:0b8cde8c47998c118d8ff285a04977a3abcf734caf579c520fca469df1cd0aac", size = 3635, upload-time = "2024-05-27T12:20:58.873Z" } wheels = [ @@ -565,7 +565,7 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -675,7 +675,7 @@ name = "cryptography" version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } wheels = [ @@ -717,20 +717,20 @@ wheels = [ [[package]] name = "cs-util" version = "0.2.2" -source = { git = "https://github.com/CosmoStat/cs_util?branch=develop#bedcdbf50ac57fc4d0b72a62b251373cd1807824" } +source = { git = "https://github.com/CosmoStat/cs_util?branch=develop#1b15a5554e378226d98ae7bfe94e7db05573edf0" } dependencies = [ - { name = "astropy" }, - { name = "camb" }, - { name = "datetime" }, - { name = "healpy" }, - { name = "healsparse" }, - { name = "keyring" }, - { name = "matplotlib" }, - { name = "numpy" }, - { name = "pyccl" }, - { name = "scipy" }, - { name = "swig" }, - { name = "vos" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "camb", marker = "sys_platform == 'linux'" }, + { name = "datetime", marker = "sys_platform == 'linux'" }, + { name = "healpy", marker = "sys_platform == 'linux'" }, + { name = "healsparse", marker = "sys_platform == 'linux'" }, + { name = "keyring", marker = "sys_platform == 'linux'" }, + { name = "matplotlib", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pyccl", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "swig", marker = "sys_platform == 'linux'" }, + { name = "vos", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -747,13 +747,13 @@ name = "dask" version = "2026.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "cloudpickle" }, - { name = "fsspec" }, - { name = "packaging" }, - { name = "partd" }, - { name = "pyyaml" }, - { name = "toolz" }, + { name = "click", marker = "sys_platform == 'linux'" }, + { name = "cloudpickle", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "partd", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "toolz", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/a7/6b3c7ac32b642fbbe0821111654e0bd8cfbe88f68560bcf23cc78ab35c71/dask-2026.8.0.tar.gz", hash = "sha256:8a94c37b5de6d869343340dc26c3c3acca7ec48a3abdabe00ea3abb1125884d5", size = 11561752, upload-time = "2026-08-24T19:21:25.906Z" } wheels = [ @@ -762,7 +762,7 @@ wheels = [ [package.optional-dependencies] array = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -770,11 +770,11 @@ name = "dask-image" version = "2026.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dask", extra = ["array"] }, - { name = "numpy" }, - { name = "pims" }, - { name = "scipy" }, - { name = "tifffile" }, + { name = "dask", extra = ["array"], marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pims", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "tifffile", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/49/e592a13a5e1efdcdb8f1faab7c4e309c61648792e276f9ced5fb79381b33/dask_image-2026.5.0.tar.gz", hash = "sha256:ed6b462277e691b2c12b0890ba801a0f9a00cc1894b0aa71b195a7a1419b2b00", size = 80457, upload-time = "2026-05-27T14:05:57.383Z" } wheels = [ @@ -786,8 +786,8 @@ name = "datetime" version = "6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytz" }, - { name = "zope-interface" }, + { name = "pytz", marker = "sys_platform == 'linux'" }, + { name = "zope-interface", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/32/decbfd165e9985ba9d8c2d34a39afe5aeba2fc3fe390eb6e9ef1aab98fa8/datetime-6.0.tar.gz", hash = "sha256:c1514936d2f901e10c8e08d83bf04e6c9dbd7ca4f244da94fec980980a3bc4d5", size = 64167, upload-time = "2025-11-25T08:00:34.586Z" } wheels = [ @@ -838,7 +838,7 @@ name = "donfig" version = "0.8.1.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/71/80cc718ff6d7abfbabacb1f57aaa42e9c1552bfdd01e64ddd704e4a03638/donfig-0.8.1.post1.tar.gz", hash = "sha256:3bef3413a4c1c601b585e8d297256d0c1470ea012afa6e8461dc28bfb7c23f52", size = 19506, upload-time = "2024-05-23T14:14:31.513Z" } wheels = [ @@ -877,7 +877,7 @@ name = "fitsio" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/fd/53bfe8986f7b242fb5b66ddc9344fe67614102f44438415b0a8b32cf7f0a/fitsio-1.4.2.tar.gz", hash = "sha256:92a02f0e63d539d85ca5a185ae0cc8d40029270858275964ee2539ee0136f0c3", size = 4178786, upload-time = "2026-07-16T12:31:06.723Z" } wheels = [ @@ -948,11 +948,11 @@ name = "galsim" version = "2.8.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "lsstdesc-coord" }, - { name = "numpy" }, - { name = "pybind11" }, - { name = "setuptools" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "lsstdesc-coord", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pybind11", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/41/358f965c0512531be47c49afe7bdc1ad264b95cf5b9dacb306cad3da82b0/galsim-2.8.5.tar.gz", hash = "sha256:e449bb0d30ece16c3ee7345846b01f0c54e482e37755b623c17174881cfe5ad3", size = 8590974, upload-time = "2026-08-05T03:40:06.337Z" } wheels = [ @@ -971,7 +971,7 @@ name = "gitdb" version = "4.0.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smmap" }, + { name = "smmap", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ @@ -983,7 +983,7 @@ name = "gitpython" version = "3.1.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "gitdb" }, + { name = "gitdb", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/61/3285044215fb596bf093e39ccb96ece0a1076a8ca57a61e069a6a33cdb1b/gitpython-3.1.61.tar.gz", hash = "sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95", size = 231680, upload-time = "2026-08-28T11:01:13.761Z" } wheels = [ @@ -1056,8 +1056,8 @@ name = "h2" version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hpack" }, - { name = "hyperframe" }, + { name = "hpack", marker = "sys_platform == 'linux'" }, + { name = "hyperframe", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } wheels = [ @@ -1069,7 +1069,7 @@ name = "h5py" version = "3.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ @@ -1096,8 +1096,8 @@ name = "healpy" version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "numpy" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/54/dd2fa7e6b12f8d3bcc34d61c72cbc1bbbad9b3142583d40803825abea2f6/healpy-1.20.0.tar.gz", hash = "sha256:03b0e1551ae235c7290e9bfdf69fa9cdc194f8a2be51198c1924b15d826102ed", size = 66326598, upload-time = "2026-07-23T20:15:51.281Z" } wheels = [ @@ -1120,8 +1120,8 @@ name = "healsparse" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hpgeom" }, - { name = "numpy" }, + { name = "hpgeom", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/d6/c299485e7bc16779e33824d838395a7339565d193e017ca9aecbfc0a8269/healsparse-1.14.0.tar.gz", hash = "sha256:60e94c8a12ca3af80cfbba7b7b11c30be1c6b5585132dd9f8ea93c7935887198", size = 133778, upload-time = "2026-08-21T04:33:43.11Z" } wheels = [ @@ -1142,7 +1142,7 @@ name = "hpgeom" version = "1.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/72/27640de7d85a566b8ecd27e7894f371a19669c56c268756e18111a137df8/hpgeom-1.5.4.tar.gz", hash = "sha256:85ac73e267c11f3f248920d3541a0e6ffab821cae54f0d44414d1939dfb91200", size = 153906, upload-time = "2026-02-24T19:03:01.954Z" } wheels = [ @@ -1168,8 +1168,8 @@ name = "html5lib" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, - { name = "webencodings" }, + { name = "six", marker = "sys_platform == 'linux'" }, + { name = "webencodings", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" } wheels = [ @@ -1181,8 +1181,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "certifi", marker = "sys_platform == 'linux'" }, + { name = "h11", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -1194,10 +1194,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, + { name = "anyio", marker = "sys_platform == 'linux'" }, + { name = "certifi", marker = "sys_platform == 'linux'" }, + { name = "httpcore", marker = "sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -1206,7 +1206,7 @@ wheels = [ [package.optional-dependencies] http2 = [ - { name = "h2" }, + { name = "h2", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1241,7 +1241,7 @@ name = "hypothesis" version = "6.167.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sortedcontainers" }, + { name = "sortedcontainers", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8cee74c1390b2932406faaab76980f18946f258fa5a8afca17189b3bc655/hypothesis-6.167.1.tar.gz", hash = "sha256:62eefcb4d2791423626e9901c3027a6e0c5ffda2ac0b44b3c7e797ab9d2d5a4c", size = 505849, upload-time = "2026-08-30T19:53:09.05Z" } wheels = [ @@ -1291,7 +1291,7 @@ name = "id" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "urllib3" }, + { name = "urllib3", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } wheels = [ @@ -1312,8 +1312,8 @@ name = "imageio" version = "2.37.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "pillow" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/62/aa770a9307508d2a2a2c62d536a49347bffe9e55322db27838d3c93d0b07/imageio-2.37.4.tar.gz", hash = "sha256:e45cbc5e83502047fb138f7f585f7f105a136a57eea5f4b3cfc6ce1b52720bd3", size = 390173, upload-time = "2026-07-20T05:26:11.369Z" } wheels = [ @@ -1350,7 +1350,7 @@ name = "importlib-metadata" version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } wheels = [ @@ -1371,18 +1371,18 @@ name = "ipykernel" version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "comm" }, - { name = "debugpy" }, - { name = "ipython" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "matplotlib-inline" }, - { name = "nest-asyncio2" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, + { name = "comm", marker = "sys_platform == 'linux'" }, + { name = "debugpy", marker = "sys_platform == 'linux'" }, + { name = "ipython", marker = "sys_platform == 'linux'" }, + { name = "jupyter-client", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "matplotlib-inline", marker = "sys_platform == 'linux'" }, + { name = "nest-asyncio2", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "psutil", marker = "sys_platform == 'linux'" }, + { name = "pyzmq", marker = "sys_platform == 'linux'" }, + { name = "tornado", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } wheels = [ @@ -1394,15 +1394,15 @@ name = "ipython" version = "9.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect" }, - { name = "prompt-toolkit" }, - { name = "psutil" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, + { name = "ipython-pygments-lexers", marker = "sys_platform == 'linux'" }, + { name = "jedi", marker = "sys_platform == 'linux'" }, + { name = "matplotlib-inline", marker = "sys_platform == 'linux'" }, + { name = "pexpect", marker = "sys_platform == 'linux'" }, + { name = "prompt-toolkit", marker = "sys_platform == 'linux'" }, + { name = "psutil", marker = "sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "stack-data", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/bc/e05ae123712ce4e1fde4408eedca1791fc1ff832684565132ea1dc646092/ipython-9.17.0.tar.gz", hash = "sha256:1dc69e6966b270fb259f676c71a21450e63607729b14a672b942914a54e8b730", size = 4538547, upload-time = "2026-08-28T09:00:58.233Z" } wheels = [ @@ -1414,7 +1414,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1426,7 +1426,7 @@ name = "isoduration" version = "20.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "arrow" }, + { name = "arrow", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } wheels = [ @@ -1438,7 +1438,7 @@ name = "jaraco-classes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "more-itertools" }, + { name = "more-itertools", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ @@ -1459,7 +1459,7 @@ name = "jaraco-functools" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "more-itertools" }, + { name = "more-itertools", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } wheels = [ @@ -1471,7 +1471,7 @@ name = "jedi" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "parso" }, + { name = "parso", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ @@ -1492,7 +1492,7 @@ name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe" }, + { name = "markupsafe", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ @@ -1531,10 +1531,10 @@ name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, + { name = "attrs", marker = "sys_platform == 'linux'" }, + { name = "jsonschema-specifications", marker = "sys_platform == 'linux'" }, + { name = "referencing", marker = "sys_platform == 'linux'" }, + { name = "rpds-py", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1543,15 +1543,15 @@ wheels = [ [package.optional-dependencies] format-nongpl = [ - { name = "fqdn" }, - { name = "idna" }, - { name = "isoduration" }, - { name = "jsonpointer" }, - { name = "rfc3339-validator" }, - { name = "rfc3986-validator" }, - { name = "rfc3987-syntax" }, - { name = "uri-template" }, - { name = "webcolors" }, + { name = "fqdn", marker = "sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "isoduration", marker = "sys_platform == 'linux'" }, + { name = "jsonpointer", marker = "sys_platform == 'linux'" }, + { name = "rfc3339-validator", marker = "sys_platform == 'linux'" }, + { name = "rfc3986-validator", marker = "sys_platform == 'linux'" }, + { name = "rfc3987-syntax", marker = "sys_platform == 'linux'" }, + { name = "uri-template", marker = "sys_platform == 'linux'" }, + { name = "webcolors", marker = "sys_platform == 'linux'" }, ] [[package]] @@ -1559,7 +1559,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing" }, + { name = "referencing", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -1571,8 +1571,8 @@ name = "jupyter-builder" version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-core" }, - { name = "traitlets" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/e1/e4ef07e2be228271b59e011a746d97feef69577af1f465b1cff0f1d7c760/jupyter_builder-1.2.2.tar.gz", hash = "sha256:b6cea88f58e44b2c5eba96f28d2e0d16fd453d3ca6dc9c4492ff8a1f2e97f601", size = 981074, upload-time = "2026-08-07T06:47:18.742Z" } wheels = [ @@ -1584,12 +1584,12 @@ name = "jupyter-client" version = "8.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-core" }, - { name = "python-dateutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, - { name = "typing-extensions" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "python-dateutil", marker = "sys_platform == 'linux'" }, + { name = "pyzmq", marker = "sys_platform == 'linux'" }, + { name = "tornado", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653, upload-time = "2026-08-28T12:17:10.854Z" } wheels = [ @@ -1601,8 +1601,8 @@ name = "jupyter-core" version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs" }, - { name = "traitlets" }, + { name = "platformdirs", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ @@ -1614,14 +1614,14 @@ name = "jupyter-events" version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema", extra = ["format-nongpl"] }, - { name = "packaging" }, - { name = "python-json-logger" }, - { name = "pyyaml" }, - { name = "referencing" }, - { name = "rfc3339-validator" }, - { name = "rfc3986-validator" }, - { name = "traitlets" }, + { name = "jsonschema", extra = ["format-nongpl"], marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "python-json-logger", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "referencing", marker = "sys_platform == 'linux'" }, + { name = "rfc3339-validator", marker = "sys_platform == 'linux'" }, + { name = "rfc3986-validator", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } wheels = [ @@ -1633,7 +1633,7 @@ name = "jupyter-lsp" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-server" }, + { name = "jupyter-server", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } wheels = [ @@ -1645,23 +1645,23 @@ name = "jupyter-server" version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "argon2-cffi" }, - { name = "jinja2" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "jupyter-events" }, - { name = "jupyter-server-terminals" }, - { name = "nbconvert" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "prometheus-client" }, - { name = "pyzmq" }, - { name = "send2trash" }, - { name = "terminado" }, - { name = "tornado" }, - { name = "traitlets" }, - { name = "websocket-client" }, + { name = "anyio", marker = "sys_platform == 'linux'" }, + { name = "argon2-cffi", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "jupyter-client", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "jupyter-events", marker = "sys_platform == 'linux'" }, + { name = "jupyter-server-terminals", marker = "sys_platform == 'linux'" }, + { name = "nbconvert", marker = "sys_platform == 'linux'" }, + { name = "nbformat", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "prometheus-client", marker = "sys_platform == 'linux'" }, + { name = "pyzmq", marker = "sys_platform == 'linux'" }, + { name = "send2trash", marker = "sys_platform == 'linux'" }, + { name = "terminado", marker = "sys_platform == 'linux'" }, + { name = "tornado", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "websocket-client", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/e0/63b481d5b21f81fe23173dfc9eb25629fa1bc174fdc4a2c19d09c1ff8124/jupyter_server-2.21.0.tar.gz", hash = "sha256:70d9a1883f57d3576ea17f4ce061ec1a7aad7ef388d00428cfb7f5e4f0022271", size = 760357, upload-time = "2026-08-27T16:06:34.049Z" } wheels = [ @@ -1673,7 +1673,7 @@ name = "jupyter-server-terminals" version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "terminado" }, + { name = "terminado", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } wheels = [ @@ -1685,19 +1685,19 @@ name = "jupyterlab" version = "4.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "async-lru" }, - { name = "httpx" }, - { name = "ipykernel" }, - { name = "jinja2" }, - { name = "jupyter-builder" }, - { name = "jupyter-core" }, - { name = "jupyter-lsp" }, - { name = "jupyter-server" }, - { name = "jupyterlab-server" }, - { name = "notebook-shim" }, - { name = "packaging" }, - { name = "tornado" }, - { name = "traitlets" }, + { name = "async-lru", marker = "sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'linux'" }, + { name = "ipykernel", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "jupyter-builder", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "jupyter-lsp", marker = "sys_platform == 'linux'" }, + { name = "jupyter-server", marker = "sys_platform == 'linux'" }, + { name = "jupyterlab-server", marker = "sys_platform == 'linux'" }, + { name = "notebook-shim", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "tornado", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/c0/45934229995d4c6e38192ca118d93185f60808f64157e3df3c5c28f8c5fd/jupyterlab-4.6.3.tar.gz", hash = "sha256:2e3db6e3a12495ebd188276e985bf5ac502fbde3d1e8628819920210008de498", size = 28319771, upload-time = "2026-08-10T18:50:57.947Z" } wheels = [ @@ -1718,13 +1718,13 @@ name = "jupyterlab-server" version = "2.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "babel" }, - { name = "jinja2" }, - { name = "json5" }, - { name = "jsonschema" }, - { name = "jupyter-server" }, - { name = "packaging" }, - { name = "requests" }, + { name = "babel", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "json5", marker = "sys_platform == 'linux'" }, + { name = "jsonschema", marker = "sys_platform == 'linux'" }, + { name = "jupyter-server", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } wheels = [ @@ -1736,11 +1736,11 @@ name = "keyring" version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jaraco-classes" }, - { name = "jaraco-context" }, - { name = "jaraco-functools" }, - { name = "jeepney" }, - { name = "secretstorage" }, + { name = "jaraco-classes", marker = "sys_platform == 'linux'" }, + { name = "jaraco-context", marker = "sys_platform == 'linux'" }, + { name = "jaraco-functools", marker = "sys_platform == 'linux'" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ @@ -1848,7 +1848,7 @@ name = "lazy-loader" version = "0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } wheels = [ @@ -1885,8 +1885,8 @@ name = "lsstdesc-coord" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future" }, - { name = "numpy" }, + { name = "future", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/e0/6806df4cfa54927a2d8e425407611b4fc031dd125bc952b4a31deb743305/lsstdesc_coord-1.3.1.tar.gz", hash = "sha256:60f878c29e1f30a9b50bf60dca3c466dc9cfb2cbc71f0a27c575ced969de57ab", size = 41647, upload-time = "2026-02-13T18:37:32.611Z" } wheels = [ @@ -1978,7 +1978,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl" }, + { name = "mdurl", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -2028,15 +2028,15 @@ name = "matplotlib" version = "3.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "contourpy" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, + { name = "contourpy", marker = "sys_platform == 'linux'" }, + { name = "cycler", marker = "sys_platform == 'linux'" }, + { name = "fonttools", marker = "sys_platform == 'linux'" }, + { name = "kiwisolver", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "pyparsing", marker = "sys_platform == 'linux'" }, + { name = "python-dateutil", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } wheels = [ @@ -2062,7 +2062,7 @@ name = "matplotlib-inline" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "traitlets" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ @@ -2074,11 +2074,11 @@ name = "mccd" version = "1.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "modopt" }, - { name = "numpy" }, - { name = "python-pysap" }, - { name = "scipy" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "modopt", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "python-pysap", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/b4/2d5618bff92bcc1b928dcc356cbce19cd4ade689d046ba0267da10247f38/mccd-1.2.4.tar.gz", hash = "sha256:e146c84867f4f97e58a0d3de6ead25feceb46f58a1a24e29d102007723e8719f", size = 75762, upload-time = "2023-02-17T19:09:17.039Z" } wheels = [ @@ -2090,7 +2090,7 @@ name = "mdit-py-plugins" version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, + { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ @@ -2120,10 +2120,10 @@ name = "modopt" version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, - { name = "numpy" }, - { name = "scipy" }, - { name = "tqdm" }, + { name = "importlib-metadata", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/cf/7665c4990b8623b9e27733edd17a69c63e2d35c5f05d1a253d5ed2f98e52/modopt-1.7.2.tar.gz", hash = "sha256:d5e8edd935b813c3677beeed3245ef4894e7ac09e180150368eda044914488b4", size = 778069, upload-time = "2024-04-12T10:22:11.24Z" } wheels = [ @@ -2173,12 +2173,12 @@ name = "myst-parser" version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx" }, + { name = "docutils", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, + { name = "mdit-py-plugins", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } wheels = [ @@ -2199,10 +2199,10 @@ name = "nbclient" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "nbformat" }, - { name = "traitlets" }, + { name = "jupyter-client", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "nbformat", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } wheels = [ @@ -2214,20 +2214,20 @@ name = "nbconvert" version = "7.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "beautifulsoup4" }, - { name = "bleach", extra = ["css"] }, - { name = "defusedxml" }, - { name = "jinja2" }, - { name = "jupyter-core" }, - { name = "jupyterlab-pygments" }, - { name = "markupsafe" }, - { name = "mistune" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "pandocfilters" }, - { name = "pygments" }, - { name = "traitlets" }, + { name = "beautifulsoup4", marker = "sys_platform == 'linux'" }, + { name = "bleach", extra = ["css"], marker = "sys_platform == 'linux'" }, + { name = "defusedxml", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "jupyterlab-pygments", marker = "sys_platform == 'linux'" }, + { name = "markupsafe", marker = "sys_platform == 'linux'" }, + { name = "mistune", marker = "sys_platform == 'linux'" }, + { name = "nbclient", marker = "sys_platform == 'linux'" }, + { name = "nbformat", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pandocfilters", marker = "sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } wheels = [ @@ -2239,10 +2239,10 @@ name = "nbformat" version = "5.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastjsonschema" }, - { name = "jsonschema" }, - { name = "jupyter-core" }, - { name = "traitlets" }, + { name = "fastjsonschema", marker = "sys_platform == 'linux'" }, + { name = "jsonschema", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core", marker = "sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775, upload-time = "2026-08-17T08:10:51.942Z" } wheels = [ @@ -2303,9 +2303,9 @@ name = "nibabel" version = "5.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/01/3d2cc510c616bc8e27be17a063070d9126f69407961594a9ae734ea51121/nibabel-5.4.2.tar.gz", hash = "sha256:d5f4b9076a13178ae7f7acf18c8dbd503ee1c4d5c0c23b85df7be87efcbb49da", size = 4663132, upload-time = "2026-03-11T13:31:52.42Z" } wheels = [ @@ -2317,7 +2317,7 @@ name = "notebook-shim" version = "0.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-server" }, + { name = "jupyter-server", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } wheels = [ @@ -2329,8 +2329,8 @@ name = "numba" version = "0.67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "llvmlite" }, - { name = "numpy" }, + { name = "llvmlite", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/90/2544f4e3a61e501d6c9a5418fd4b905323222693d54a02cab0106a0af865/numba-0.67.0.tar.gz", hash = "sha256:cd75aa535b33fa05d9d930b1ae8af9f97a2881e96d72dfb38ec9b78284d9f851", size = 2836515, upload-time = "2026-08-11T23:04:00.174Z" } wheels = [ @@ -2349,8 +2349,8 @@ name = "numcodecs" version = "0.16.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "typing-extensions" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/bd/8a391e7c356366224734efd24da929cc4796fff468bfb179fe1af6548535/numcodecs-0.16.5.tar.gz", hash = "sha256:0d0fb60852f84c0bd9543cc4d2ab9eefd37fc8efcc410acd4777e62a1d300318", size = 6276387, upload-time = "2025-11-21T02:49:48.986Z" } wheels = [ @@ -2399,7 +2399,7 @@ name = "numpydoc" version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx" }, + { name = "sphinx", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } wheels = [ @@ -2420,8 +2420,8 @@ name = "pandas" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "python-dateutil", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } wheels = [ @@ -2467,8 +2467,8 @@ name = "partd" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "locket" }, - { name = "toolz" }, + { name = "locket", marker = "sys_platform == 'linux'" }, + { name = "toolz", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } wheels = [ @@ -2480,7 +2480,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2533,11 +2533,11 @@ name = "pims" version = "0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "imageio" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "slicerator" }, - { name = "tifffile" }, + { name = "imageio", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "slicerator", marker = "sys_platform == 'linux'" }, + { name = "tifffile", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b183011c08541c5039ba3d04f5316c70312b48a8e003a9/pims-0.7.tar.gz", hash = "sha256:55907a4c301256086d2aa4e34a5361b9109f24e375c2071e1117b9491e82946b", size = 87779, upload-time = "2024-06-10T19:20:42.842Z" } @@ -2564,7 +2564,7 @@ name = "progressbar2" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-utils" }, + { name = "python-utils", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/1e/5bd376abe32a392e74fffd82feb42ee1472d263bd358d628af20663d0346/progressbar2-4.6.0.tar.gz", hash = "sha256:fe48c8955a84428af77bff2642ba47041e1b8f7c867a5b7cc94f8bc255a8f0cf", size = 542464, upload-time = "2026-08-14T01:07:30.008Z" } wheels = [ @@ -2585,7 +2585,7 @@ name = "prompt-toolkit" version = "3.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wcwidth" }, + { name = "wcwidth", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ @@ -2658,8 +2658,8 @@ name = "pybtex" version = "0.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "latexcodec" }, - { name = "pyyaml" }, + { name = "latexcodec", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/f5/f30da9c93f0fa6d619332b2f69597219b625f35780473a05164a9981fd9a/pybtex-0.26.1.tar.gz", hash = "sha256:2e5543bea424e60e9e42eef70bff597be48649d8f68ba061a7a092b2477d5464", size = 692991, upload-time = "2026-04-03T13:05:39.014Z" } wheels = [ @@ -2671,8 +2671,8 @@ name = "pybtex-docutils" version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, - { name = "pybtex" }, + { name = "docutils", marker = "sys_platform == 'linux'" }, + { name = "pybtex", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/84/796ea94d26188a853660f81bded39f8de4cfe595130aef0dea1088705a11/pybtex-docutils-1.0.3.tar.gz", hash = "sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b", size = 18348, upload-time = "2023-08-22T18:47:54.833Z" } wheels = [ @@ -2684,10 +2684,10 @@ name = "pyccl" version = "3.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "scipy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/76/dc091056f1af48e6cb56d651dac6eb8494dd2515547d9543c0ce5365b873/pyccl-3.3.6.tar.gz", hash = "sha256:fd6c0181381eb57345ec9b56bdcf9300c9a31a42014aa9c8ced95f31ace16795", size = 16790489, upload-time = "2026-07-28T09:29:17.738Z" } wheels = [ @@ -2710,10 +2710,10 @@ name = "pydantic" version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "sys_platform == 'linux'" }, + { name = "pydantic-core", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ @@ -2725,7 +2725,7 @@ name = "pydantic-core" version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } wheels = [ @@ -2778,9 +2778,9 @@ name = "pydantic-settings" version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "python-dotenv", marker = "sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ @@ -2792,14 +2792,14 @@ name = "pydata-sphinx-theme" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "accessible-pygments" }, - { name = "babel" }, - { name = "beautifulsoup4" }, - { name = "docutils" }, - { name = "jinja2" }, - { name = "pygments" }, - { name = "requests" }, - { name = "sphinx" }, + { name = "accessible-pygments", marker = "sys_platform == 'linux'" }, + { name = "babel", marker = "sys_platform == 'linux'" }, + { name = "beautifulsoup4", marker = "sys_platform == 'linux'" }, + { name = "docutils", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/8e/add936feaaa9dade7d5b87c6852566d85e518b38ad32224dea32ef958984/pydata_sphinx_theme-0.20.0.tar.gz", hash = "sha256:0da172d41e19a66de875f4002f7054b385372ec65763852193791e658d50bb4a", size = 5004756, upload-time = "2026-07-09T09:09:14.693Z" } wheels = [ @@ -2811,7 +2811,7 @@ name = "pyerfa" version = "2.0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/39/63cc8291b0cf324ae710df41527faf7d331bce573899199d926b3e492260/pyerfa-2.0.1.5.tar.gz", hash = "sha256:17d6b24fe4846c65d5e7d8c362dcb08199dc63b30a236aedd73875cc83e1f6c0", size = 818430, upload-time = "2024-11-11T15:22:30.852Z" } wheels = [ @@ -2834,8 +2834,8 @@ name = "pyopenssl" version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "cryptography", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } wheels = [ @@ -2865,8 +2865,8 @@ name = "pyqt5" version = "5.15.11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyqt5-qt5" }, - { name = "pyqt5-sip" }, + { name = "pyqt5-qt5", marker = "sys_platform == 'linux'" }, + { name = "pyqt5-sip", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } wheels = [ @@ -2897,8 +2897,8 @@ name = "pyqtgraph" version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama" }, - { name = "numpy" }, + { name = "colorama", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, @@ -2909,10 +2909,10 @@ name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, + { name = "iniconfig", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pluggy", marker = "sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -2924,9 +2924,9 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, - { name = "pluggy" }, - { name = "pytest" }, + { name = "coverage", marker = "sys_platform == 'linux'" }, + { name = "pluggy", marker = "sys_platform == 'linux'" }, + { name = "pytest", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ @@ -2938,7 +2938,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "six", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -2968,16 +2968,16 @@ name = "python-pysap" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "matplotlib" }, - { name = "modopt" }, - { name = "nibabel" }, - { name = "numpy" }, - { name = "progressbar2" }, - { name = "pywavelets" }, - { name = "scikit-image" }, - { name = "scikit-learn" }, - { name = "scipy" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "matplotlib", marker = "sys_platform == 'linux'" }, + { name = "modopt", marker = "sys_platform == 'linux'" }, + { name = "nibabel", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "progressbar2", marker = "sys_platform == 'linux'" }, + { name = "pywavelets", marker = "sys_platform == 'linux'" }, + { name = "scikit-image", marker = "sys_platform == 'linux'" }, + { name = "scikit-learn", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/90/29b10f180876a322c7cfbadd2552efef34374dceb618fdc3f48ef792666e/python_pysap-0.3.0.tar.gz", hash = "sha256:55cdfec05eacc410723f31df754b67728224935aef7d4332b89ef6e90f2aab97", size = 775664, upload-time = "2026-01-06T10:12:58.287Z" } @@ -2986,7 +2986,7 @@ name = "python-utils" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5d/71/ec6665d4ce42ee5a59fffd31a4d5164f92da15ccb8c758dba13d2419ea53/python_utils-4.0.1.tar.gz", hash = "sha256:4e8e8ecaba3862f843a60c1982c99cda23b522f417006a807996a876c18beb8d", size = 43765, upload-time = "2026-08-30T19:26:04.591Z" } wheels = [ @@ -3007,8 +3007,8 @@ name = "pyvo" version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "requests" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/35/12c0f4fa0879316837ac56f275942f006f0735e5aedba529b4449dddc36f/pyvo-1.9.1.tar.gz", hash = "sha256:2f26c99af7c32f3c34b919e2d14eaf1a95914176d693fb7769773f3ab0b7999d", size = 2167800, upload-time = "2026-06-11T14:44:01.349Z" } wheels = [ @@ -3020,7 +3020,7 @@ name = "pywavelets" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/75/50581633d199812205ea8cdd0f6d52f12a624886b74bf1486335b67f01ff/pywavelets-1.9.0.tar.gz", hash = "sha256:148d12203377772bea452a59211d98649c8ee4a05eff019a9021853a36babdc8", size = 3938340, upload-time = "2025-08-04T16:20:04.978Z" } wheels = [ @@ -3079,7 +3079,7 @@ name = "pyzmq" version = "27.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, + { name = "cffi", marker = "implementation_name == 'pypy' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" } wheels = [ @@ -3109,7 +3109,7 @@ name = "questionary" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "prompt-toolkit" }, + { name = "prompt-toolkit", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } wheels = [ @@ -3121,9 +3121,9 @@ name = "readme-renderer" version = "46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, - { name = "nh3" }, - { name = "pygments" }, + { name = "docutils", marker = "sys_platform == 'linux'" }, + { name = "nh3", marker = "sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382, upload-time = "2026-08-28T15:18:32.49Z" } wheels = [ @@ -3135,9 +3135,9 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "attrs", marker = "sys_platform == 'linux'" }, + { name = "rpds-py", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -3149,16 +3149,16 @@ name = "reproject" version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy" }, - { name = "astropy-healpix" }, - { name = "dask", extra = ["array"] }, - { name = "dask-image" }, - { name = "fsspec" }, - { name = "numpy" }, - { name = "pillow" }, - { name = "pyavm" }, - { name = "scipy" }, - { name = "zarr" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "astropy-healpix", marker = "sys_platform == 'linux'" }, + { name = "dask", extra = ["array"], marker = "sys_platform == 'linux'" }, + { name = "dask-image", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "pyavm", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "zarr", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/44/6fd820ba336484277a91a2f4808b60d6ec0b9f033f588c237e778f20fe89/reproject-0.21.0.tar.gz", hash = "sha256:01ede715a1993c29431f52ff74189ef30f5e7b2e8b4dc88c1b002145a971dc1c", size = 1622661, upload-time = "2026-06-25T15:11:34.886Z" } wheels = [ @@ -3171,10 +3171,10 @@ name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, + { name = "certifi", marker = "sys_platform == 'linux'" }, + { name = "charset-normalizer", marker = "sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "urllib3", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -3186,7 +3186,7 @@ name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests" }, + { name = "requests", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ @@ -3198,7 +3198,7 @@ name = "rfc3339-validator" version = "0.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "six", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } wheels = [ @@ -3228,7 +3228,7 @@ name = "rfc3987-syntax" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "lark" }, + { name = "lark", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } wheels = [ @@ -3240,8 +3240,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, + { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -3350,14 +3350,14 @@ name = "scikit-image" version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "imageio" }, - { name = "lazy-loader" }, - { name = "networkx" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "scipy" }, - { name = "tifffile" }, + { name = "imageio", marker = "sys_platform == 'linux'" }, + { name = "lazy-loader", marker = "sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "tifffile", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } wheels = [ @@ -3388,11 +3388,11 @@ name = "scikit-learn" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "joblib" }, - { name = "narwhals" }, - { name = "numpy" }, - { name = "scipy" }, - { name = "threadpoolctl" }, + { name = "joblib", marker = "sys_platform == 'linux'" }, + { name = "narwhals", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "threadpoolctl", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } wheels = [ @@ -3411,7 +3411,7 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -3442,8 +3442,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "jeepney" }, + { name = "cryptography", marker = "sys_platform == 'linux'" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -3482,9 +3482,9 @@ name = "sf-tools" version = "2.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future" }, - { name = "modopt" }, - { name = "numpy" }, + { name = "future", marker = "sys_platform == 'linux'" }, + { name = "modopt", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/41/6281d12b863867fc7d7db0aeb55db7ec94fb8f35873b9940edeeb3c29d0f/sf_tools-2.0.4.tar.gz", hash = "sha256:607204b7369cb381c02ba95f49f13d2733877a625026fac340dcddcca6700d24", size = 10426, upload-time = "2019-08-22T08:41:46.961Z" } @@ -3493,78 +3493,87 @@ name = "shapepipe" version = "1.1.0" source = { editable = "." } dependencies = [ - { name = "astropy" }, - { name = "astroquery" }, - { name = "canfar" }, - { name = "cs-util" }, - { name = "galsim" }, - { name = "h5py" }, - { name = "joblib" }, - { name = "matplotlib" }, - { name = "mccd" }, - { name = "modopt" }, - { name = "mpi4py" }, - { name = "ngmix" }, - { name = "numba" }, - { name = "numpy" }, - { name = "pandas" }, - { name = "pyqt5" }, - { name = "pyqtgraph" }, - { name = "python-dateutil" }, - { name = "python-pysap" }, - { name = "reproject" }, - { name = "sf-tools" }, - { name = "skaha" }, - { name = "sqlitedict" }, - { name = "termcolor" }, - { name = "tqdm" }, - { name = "vos" }, + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "astroquery", marker = "sys_platform == 'linux'" }, + { name = "canfar", marker = "sys_platform == 'linux'" }, + { name = "cs-util", marker = "sys_platform == 'linux'" }, + { name = "galsim", marker = "sys_platform == 'linux'" }, + { name = "h5py", marker = "sys_platform == 'linux'" }, + { name = "healsparse", marker = "sys_platform == 'linux'" }, + { name = "hpgeom", marker = "sys_platform == 'linux'" }, + { name = "joblib", marker = "sys_platform == 'linux'" }, + { name = "matplotlib", marker = "sys_platform == 'linux'" }, + { name = "mccd", marker = "sys_platform == 'linux'" }, + { name = "modopt", marker = "sys_platform == 'linux'" }, + { name = "mpi4py", marker = "sys_platform == 'linux'" }, + { name = "ngmix", marker = "sys_platform == 'linux'" }, + { name = "numba", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pandas", marker = "sys_platform == 'linux'" }, + { name = "pyqt5", marker = "sys_platform == 'linux'" }, + { name = "pyqtgraph", marker = "sys_platform == 'linux'" }, + { name = "python-dateutil", marker = "sys_platform == 'linux'" }, + { name = "python-pysap", marker = "sys_platform == 'linux'" }, + { name = "reproject", marker = "sys_platform == 'linux'" }, + { name = "sf-tools", marker = "sys_platform == 'linux'" }, + { name = "skaha", marker = "sys_platform == 'linux'" }, + { name = "skyproj", marker = "sys_platform == 'linux'" }, + { name = "sqlitedict", marker = "sys_platform == 'linux'" }, + { name = "termcolor", marker = "sys_platform == 'linux'" }, + { name = "tqdm", marker = "sys_platform == 'linux'" }, + { name = "vos", marker = "sys_platform == 'linux'" }, ] [package.optional-dependencies] dev = [ - { name = "build" }, - { name = "fitsio" }, - { name = "hypothesis" }, - { name = "ipython" }, - { name = "jupyterlab" }, - { name = "myst-parser" }, - { name = "numpydoc" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, - { name = "snakemake" }, - { name = "sphinx" }, - { name = "sphinx-book-theme" }, - { name = "sphinxcontrib-bibtex" }, - { name = "twine" }, + { name = "build", marker = "sys_platform == 'linux'" }, + { name = "fitsio", marker = "sys_platform == 'linux'" }, + { name = "hypothesis", marker = "sys_platform == 'linux'" }, + { name = "ipython", marker = "sys_platform == 'linux'" }, + { name = "jupyterlab", marker = "sys_platform == 'linux'" }, + { name = "matplotlib", marker = "sys_platform == 'linux'" }, + { name = "myst-parser", marker = "sys_platform == 'linux'" }, + { name = "numpydoc", marker = "sys_platform == 'linux'" }, + { name = "pytest", marker = "sys_platform == 'linux'" }, + { name = "pytest-cov", marker = "sys_platform == 'linux'" }, + { name = "ruff", marker = "sys_platform == 'linux'" }, + { name = "skyproj", marker = "sys_platform == 'linux'" }, + { name = "snakemake", marker = "sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'linux'" }, + { name = "sphinx-book-theme", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-bibtex", marker = "sys_platform == 'linux'" }, + { name = "twine", marker = "sys_platform == 'linux'" }, ] doc = [ - { name = "myst-parser" }, - { name = "numpydoc" }, - { name = "sphinx" }, - { name = "sphinx-book-theme" }, - { name = "sphinxcontrib-bibtex" }, + { name = "myst-parser", marker = "sys_platform == 'linux'" }, + { name = "numpydoc", marker = "sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'linux'" }, + { name = "sphinx-book-theme", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-bibtex", marker = "sys_platform == 'linux'" }, ] fitsio = [ - { name = "fitsio" }, + { name = "fitsio", marker = "sys_platform == 'linux'" }, ] jupyter = [ - { name = "ipython" }, - { name = "jupyterlab" }, - { name = "snakemake" }, + { name = "ipython", marker = "sys_platform == 'linux'" }, + { name = "jupyterlab", marker = "sys_platform == 'linux'" }, + { name = "snakemake", marker = "sys_platform == 'linux'" }, ] lint = [ - { name = "ruff" }, + { name = "ruff", marker = "sys_platform == 'linux'" }, +] +plot = [ + { name = "matplotlib", marker = "sys_platform == 'linux'" }, + { name = "skyproj", marker = "sys_platform == 'linux'" }, ] release = [ - { name = "build" }, - { name = "twine" }, + { name = "build", marker = "sys_platform == 'linux'" }, + { name = "twine", marker = "sys_platform == 'linux'" }, ] test = [ - { name = "hypothesis" }, - { name = "pytest" }, - { name = "pytest-cov" }, + { name = "hypothesis", marker = "sys_platform == 'linux'" }, + { name = "pytest", marker = "sys_platform == 'linux'" }, + { name = "pytest-cov", marker = "sys_platform == 'linux'" }, ] [package.metadata] @@ -3577,11 +3586,14 @@ requires-dist = [ { name = "fitsio", marker = "extra == 'fitsio'" }, { name = "galsim", specifier = ">=2.8" }, { name = "h5py" }, + { name = "healsparse" }, + { name = "hpgeom" }, { name = "hypothesis", marker = "extra == 'test'", specifier = ">=6.155.2" }, { name = "ipython", marker = "extra == 'jupyter'", specifier = ">=9.14.1" }, { name = "joblib", specifier = ">=1.4" }, { name = "jupyterlab", marker = "extra == 'jupyter'", specifier = ">=4.5.9" }, { name = "matplotlib", specifier = ">=3.10" }, + { name = "matplotlib", marker = "extra == 'plot'", specifier = ">=3.10" }, { name = "mccd", specifier = ">=1.2.4" }, { name = "modopt", specifier = ">=1.6" }, { name = "mpi4py", specifier = ">=4.1.2" }, @@ -3600,8 +3612,10 @@ requires-dist = [ { name = "reproject", specifier = ">=0.19" }, { name = "ruff", marker = "extra == 'lint'" }, { name = "sf-tools", specifier = ">=2.0.4" }, - { name = "shapepipe", extras = ["doc", "jupyter", "lint", "release", "test", "fitsio"], marker = "extra == 'dev'" }, + { name = "shapepipe", extras = ["doc", "jupyter", "lint", "plot", "release", "test", "fitsio"], marker = "extra == 'dev'" }, { name = "skaha", specifier = ">=1.7" }, + { name = "skyproj" }, + { name = "skyproj", marker = "extra == 'plot'" }, { name = "snakemake", marker = "extra == 'jupyter'", specifier = ">=9.22.0" }, { name = "sphinx", marker = "extra == 'doc'" }, { name = "sphinx-book-theme", marker = "extra == 'doc'" }, @@ -3612,7 +3626,7 @@ requires-dist = [ { name = "twine", marker = "extra == 'release'" }, { name = "vos", specifier = ">=3.6" }, ] -provides-extras = ["doc", "jupyter", "lint", "release", "test", "fitsio", "dev"] +provides-extras = ["doc", "jupyter", "lint", "plot", "release", "test", "fitsio", "dev"] [[package]] name = "shellingham" @@ -3637,19 +3651,36 @@ name = "skaha" version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "defusedxml" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "rich" }, - { name = "toml" }, - { name = "typer" }, - { name = "vos" }, + { name = "defusedxml", marker = "sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'linux'" }, + { name = "toml", marker = "sys_platform == 'linux'" }, + { name = "typer", marker = "sys_platform == 'linux'" }, + { name = "vos", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/76/62/a733508ff88200c41459f5cdb72a797eb8795c833b4e9d51d31b5efe4073/skaha-1.7.0.tar.gz", hash = "sha256:ef9d69e7a4da8653cdbde3e62f18caef137130064ccb6fabed57c7c6f8bf8ef9", size = 54414, upload-time = "2025-05-28T21:17:03.631Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/d9/d3/a3c1d569ae714ed9985adccefdfe2d6dd9f8ad7a297bd8fa4c7ed30c587b/skaha-1.7.0-py3-none-any.whl", hash = "sha256:68b0d3c925b98bf145c5f695237474fe1cea07cf9b69b4a9bae9910375dfa01a", size = 40795, upload-time = "2025-05-28T21:17:02.296Z" }, ] +[[package]] +name = "skyproj" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astropy", marker = "sys_platform == 'linux'" }, + { name = "healsparse", marker = "sys_platform == 'linux'" }, + { name = "hpgeom", marker = "sys_platform == 'linux'" }, + { name = "matplotlib", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/cf/5158151ae6fc60459f430451b612a4bf2da07ebff2a9f52a37e73715a5ee/skyproj-2.5.0.tar.gz", hash = "sha256:cb8d5115927ca43cacdb6d92f00bb4fcc8563ee71028f755203ce1752e67c140", size = 8521587, upload-time = "2026-06-26T19:51:15.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/b1/1d86ee4ae4ae7a47e31cc770dc83057388ef16311498283ca90046b3bd04/skyproj-2.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cbfcbd4d3d600557ab19bb5cf79b750ec552da054e3be781bce88e996b9955d", size = 4146686, upload-time = "2026-06-26T19:51:04.011Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e9/f2dd235dcb9f6c3b2fec36ef04d7dd36aef4daf9afc4ab4390d6230c96dc/skyproj-2.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78618084c9362b62ddbfd336ca1864c94d99e2a1f26d2fbf3466fd19dc702c58", size = 4146630, upload-time = "2026-06-26T19:51:10.552Z" }, +] + [[package]] name = "slicerator" version = "1.1.0" @@ -3664,7 +3695,7 @@ name = "smart-open" version = "7.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wrapt" }, + { name = "wrapt", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/c6/22e7a2acd5d27941e85e0d7ede398da5abe2e4677d2265c924157247c32e/smart_open-7.7.1.tar.gz", hash = "sha256:9414ba5733e28309f29b28a303b0f1054ad23fe0275f1a1b600c80a724f4bd1a", size = 54952, upload-time = "2026-06-26T07:56:35.309Z" } wheels = [ @@ -3685,37 +3716,37 @@ name = "snakemake" version = "9.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "conda-inject" }, - { name = "configargparse" }, - { name = "connection-pool" }, - { name = "docutils" }, - { name = "dpath" }, - { name = "gitpython" }, - { name = "humanfriendly" }, - { name = "immutables" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "platformdirs" }, - { name = "psutil" }, - { name = "pulp" }, - { name = "pyyaml" }, - { name = "referencing" }, - { name = "requests" }, - { name = "smart-open" }, - { name = "snakemake-interface-common" }, - { name = "snakemake-interface-executor-plugins" }, - { name = "snakemake-interface-logger-plugins" }, - { name = "snakemake-interface-report-plugins" }, - { name = "snakemake-interface-scheduler-plugins" }, - { name = "snakemake-interface-storage-plugins" }, - { name = "sqlmodel" }, - { name = "tabulate" }, - { name = "tenacity" }, - { name = "throttler" }, - { name = "wrapt" }, - { name = "yte" }, + { name = "conda-inject", marker = "sys_platform == 'linux'" }, + { name = "configargparse", marker = "sys_platform == 'linux'" }, + { name = "connection-pool", marker = "sys_platform == 'linux'" }, + { name = "docutils", marker = "sys_platform == 'linux'" }, + { name = "dpath", marker = "sys_platform == 'linux'" }, + { name = "gitpython", marker = "sys_platform == 'linux'" }, + { name = "humanfriendly", marker = "sys_platform == 'linux'" }, + { name = "immutables", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "jsonschema", marker = "sys_platform == 'linux'" }, + { name = "nbformat", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'linux'" }, + { name = "psutil", marker = "sys_platform == 'linux'" }, + { name = "pulp", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "referencing", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "smart-open", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-executor-plugins", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-logger-plugins", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-report-plugins", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-scheduler-plugins", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-storage-plugins", marker = "sys_platform == 'linux'" }, + { name = "sqlmodel", marker = "sys_platform == 'linux'" }, + { name = "tabulate", marker = "sys_platform == 'linux'" }, + { name = "tenacity", marker = "sys_platform == 'linux'" }, + { name = "throttler", marker = "sys_platform == 'linux'" }, + { name = "wrapt", marker = "sys_platform == 'linux'" }, + { name = "yte", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/37/f999e99357a9b30c6f36c92027ab3b6f64cade4ec54840197853297d2e04/snakemake-9.26.1.tar.gz", hash = "sha256:091b5d480c0c5eb2ef75b2568e9885091f3e2f11372de358da98ef66ef66e1e0", size = 6814489, upload-time = "2026-08-28T05:44:51.852Z" } wheels = [ @@ -3727,9 +3758,9 @@ name = "snakemake-interface-common" version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argparse-dataclass" }, - { name = "configargparse" }, - { name = "packaging" }, + { name = "argparse-dataclass", marker = "sys_platform == 'linux'" }, + { name = "configargparse", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/c3/592f832f6e5d2d31f749392e48e8401b7625dec668d3d365d8d28f2b6c30/snakemake_interface_common-1.23.0.tar.gz", hash = "sha256:6ed14531a461417659364a0dd0acc51b786af4e26fc15cc5e00ff3d9fcaffacc", size = 13960, upload-time = "2026-03-08T21:54:29.251Z" } wheels = [ @@ -3741,9 +3772,9 @@ name = "snakemake-interface-executor-plugins" version = "9.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argparse-dataclass" }, - { name = "snakemake-interface-common" }, - { name = "throttler" }, + { name = "argparse-dataclass", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, + { name = "throttler", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/50/de06b284c45a8e94fb8e4a12d5235065e78b49b8f84329dc10fe39f4b7dd/snakemake_interface_executor_plugins-9.4.0.tar.gz", hash = "sha256:9d4138897beacbaadaedad94b63f948eaeb604b7fc78f9cf65ac57f090f2c066", size = 16549, upload-time = "2026-03-08T17:04:02.644Z" } wheels = [ @@ -3755,7 +3786,7 @@ name = "snakemake-interface-logger-plugins" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "snakemake-interface-common" }, + { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/0c/3fa5d592663c65669a867526604aadc6fbc235fb9284e94b49c0ef59aa41/snakemake_interface_logger_plugins-2.1.0.tar.gz", hash = "sha256:c89a00d2a398490cecd91b6dc6db8049cba93712d82e1d8f3000f3040bf3791c", size = 15917, upload-time = "2026-05-20T15:12:35.259Z" } wheels = [ @@ -3767,7 +3798,7 @@ name = "snakemake-interface-report-plugins" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "snakemake-interface-common" }, + { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/d6/6160ed98de665d6871dd356597dbf726688cc786e88668359ca37b7d9f54/snakemake_interface_report_plugins-1.3.0.tar.gz", hash = "sha256:fc9495298bec4e69721ab8afe6d6d88a86966fda2eeb003db56b9a88b86d5934", size = 4283, upload-time = "2025-10-31T10:52:36.55Z" } wheels = [ @@ -3779,7 +3810,7 @@ name = "snakemake-interface-scheduler-plugins" version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "snakemake-interface-common" }, + { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/d9/d480807d2cfc2d132bc760d877d45ec8fbe620a24200ec4d2697c4a26031/snakemake_interface_scheduler_plugins-2.0.2.tar.gz", hash = "sha256:2797e8fa9019d983132c2b403f14d6fcd3c5ad4c8d8a66b984b4740a71cacc46", size = 8642, upload-time = "2025-10-20T13:58:12.988Z" } wheels = [ @@ -3791,11 +3822,11 @@ name = "snakemake-interface-storage-plugins" version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "humanfriendly" }, - { name = "snakemake-interface-common" }, - { name = "tenacity" }, - { name = "throttler" }, - { name = "wrapt" }, + { name = "humanfriendly", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, + { name = "tenacity", marker = "sys_platform == 'linux'" }, + { name = "throttler", marker = "sys_platform == 'linux'" }, + { name = "wrapt", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/6e/f3c5b2d621fd6a6b78d8cfc01fef6b926fe2c277f5ed77c5e4deeacb94eb/snakemake_interface_storage_plugins-4.4.1.tar.gz", hash = "sha256:b2b5bf05318af36955ebf2ce76c921c0fb06904ca98fb30e1657d88b7b7b6945", size = 14924, upload-time = "2026-03-16T11:16:01.075Z" } wheels = [ @@ -3834,22 +3865,22 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "sys_platform == 'linux'" }, + { name = "babel", marker = "sys_platform == 'linux'" }, + { name = "docutils", marker = "sys_platform == 'linux'" }, + { name = "imagesize", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "roman-numerals", marker = "sys_platform == 'linux'" }, + { name = "snowballstemmer", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-applehelp", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-devhelp", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-htmlhelp", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-jsmath", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-qthelp", marker = "sys_platform == 'linux'" }, + { name = "sphinxcontrib-serializinghtml", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -3861,8 +3892,8 @@ name = "sphinx-book-theme" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydata-sphinx-theme" }, - { name = "sphinx" }, + { name = "pydata-sphinx-theme", marker = "sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/8a/cb008736682b3974cae6925c54939792fe56f5a167e37984cb45d8a85d8a/sphinx_book_theme-1.4.0.tar.gz", hash = "sha256:fee3cd70573d9a8cd4ab380a3cb1f11139c24dcaa2cf47d06d7f924f1b44eaff", size = 406113, upload-time = "2026-07-19T21:23:54.183Z" } wheels = [ @@ -3883,10 +3914,10 @@ name = "sphinxcontrib-bibtex" version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, - { name = "pybtex" }, - { name = "pybtex-docutils" }, - { name = "sphinx" }, + { name = "docutils", marker = "sys_platform == 'linux'" }, + { name = "pybtex", marker = "sys_platform == 'linux'" }, + { name = "pybtex-docutils", marker = "sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/15/6a/8e0b2c2420286389e7fed78ff361ec30e2f1d58c8560af8d64df5e7b61e0/sphinxcontrib_bibtex-2.7.0.tar.gz", hash = "sha256:fee700f7aae29bb8f654c62913f00d34ac44fc0b8ca0fa67ac922ff4453addee", size = 120669, upload-time = "2026-05-06T09:29:24.935Z" } wheels = [ @@ -3943,8 +3974,8 @@ name = "sqlalchemy" version = "2.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, + { name = "greenlet", marker = "(platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'WIN32' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'amd64' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 'win32' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } wheels = [ @@ -3974,8 +4005,8 @@ name = "sqlmodel" version = "0.0.37" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "sqlalchemy" }, + { name = "pydantic", marker = "sys_platform == 'linux'" }, + { name = "sqlalchemy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/26/1d2faa0fd5a765267f49751de533adac6b9ff9366c7c6e7692df4f32230f/sqlmodel-0.0.37.tar.gz", hash = "sha256:d2c19327175794faf50b1ee31cc966764f55b1dedefc046450bc5741a3d68352", size = 85527, upload-time = "2026-02-21T16:39:47.038Z" } wheels = [ @@ -3987,9 +4018,9 @@ name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, + { name = "asttokens", marker = "sys_platform == 'linux'" }, + { name = "executing", marker = "sys_platform == 'linux'" }, + { name = "pure-eval", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ @@ -4023,7 +4054,7 @@ name = "sympy" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mpmath" }, + { name = "mpmath", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ @@ -4062,8 +4093,8 @@ name = "terminado" version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "os_name != 'nt'" }, - { name = "tornado" }, + { name = "ptyprocess", marker = "os_name != 'nt' and sys_platform == 'linux'" }, + { name = "tornado", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } wheels = [ @@ -4093,7 +4124,7 @@ name = "tifffile" version = "2026.8.23" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/07/90078f49e60718d414d5440dc498301f11ff049458e65cf8d27c62a5c9d1/tifffile-2026.8.23.tar.gz", hash = "sha256:bd3c816f166f85c93329a54a0c9a1eccc9968a6a78f91d63b65d7b17675915f2", size = 446179, upload-time = "2026-08-23T18:45:05.976Z" } wheels = [ @@ -4105,7 +4136,7 @@ name = "tinycss2" version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "webencodings" }, + { name = "webencodings", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ @@ -4165,15 +4196,15 @@ name = "twine" version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "id" }, - { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, - { name = "packaging" }, - { name = "readme-renderer" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "rfc3986" }, - { name = "rich" }, - { name = "urllib3" }, + { name = "id", marker = "sys_platform == 'linux'" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "readme-renderer", marker = "sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "requests-toolbelt", marker = "sys_platform == 'linux'" }, + { name = "rfc3986", marker = "sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'linux'" }, + { name = "urllib3", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } wheels = [ @@ -4185,9 +4216,9 @@ name = "typer" version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc" }, - { name = "rich" }, - { name = "shellingham" }, + { name = "annotated-doc", marker = "sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'linux'" }, + { name = "shellingham", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } wheels = [ @@ -4208,7 +4239,7 @@ name = "typing-inspection" version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ @@ -4247,9 +4278,9 @@ name = "vos" version = "3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aenum" }, - { name = "cadcutils" }, - { name = "html2text" }, + { name = "aenum", marker = "sys_platform == 'linux'" }, + { name = "cadcutils", marker = "sys_platform == 'linux'" }, + { name = "html2text", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/71/35b07ebde34232b4f3deca8cb320c3ad615b15818fbb3b10a5209c14aadc/vos-3.7.tar.gz", hash = "sha256:073cde0ec3ffe52f283ba6505870e760492a73743986797686e7dc57d30ca563", size = 103538, upload-time = "2026-08-06T23:47:46.099Z" } wheels = [ @@ -4342,9 +4373,9 @@ name = "yte" version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argparse-dataclass" }, - { name = "dpath" }, - { name = "pyyaml" }, + { name = "argparse-dataclass", marker = "sys_platform == 'linux'" }, + { name = "dpath", marker = "sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/f5/7e44620e6e077bfe624b9a17c329b8e0d0159e176e1f1a93c2790428ab2c/yte-1.9.4.tar.gz", hash = "sha256:86a47e6d722cec9419a7ac88be57d0d6c4ce28f02860393b71a66f2c674069f6", size = 8101, upload-time = "2025-11-27T12:55:00.85Z" } wheels = [ @@ -4356,12 +4387,12 @@ name = "zarr" version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "donfig" }, - { name = "google-crc32c" }, - { name = "numcodecs" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "typing-extensions" }, + { name = "donfig", marker = "sys_platform == 'linux'" }, + { name = "google-crc32c", marker = "sys_platform == 'linux'" }, + { name = "numcodecs", marker = "sys_platform == 'linux'" }, + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/15/436cb1d3bbe86173bd44ce7a34ecb210d0c0416946e337858149a905ef5a/zarr-3.3.0.tar.gz", hash = "sha256:cd0c8cf738b4bb4807815bc1255acad5bdf1a7b7264b606c5a1bc0d0392a306b", size = 943626, upload-time = "2026-07-30T16:35:10.491Z" } wheels = [ From 6d160a2156ddc12f5afcfbd7bb0486792e6b7dbd Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 16:16:34 +0200 Subject: [PATCH 05/36] config(workflow): park the coverage-mask constants before their only carriers die MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nside_coverage=128 / nside=131072 is the production pair that makes the coverage map align pixel-wise with the UNIONS bit masks, and it is NOT the CoverageMapBuilder default (32/2048). Until now those two numbers existed in exactly two places, both slated for deletion in this retirement: scripts/sh/build_and_plot_coverage_maps.sh:25-31 and docs/source/pipeline_canfar.md:545-548. Same for the SGC/NGC plot windows (:41-51 / :555-557) and the sp_validation consumption pointer at :450-452 (notebooks/demo_apply_hsp_masks.py). Transplanted into a commented `coverage:` block in workflow/config.yaml with the bit-mask rationale intact. Commented because nothing reads it yet — the exp_footprint / coverage_map rules land in a later commit; this one only makes sure the deletion cannot take the numbers with it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- workflow/config.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/workflow/config.yaml b/workflow/config.yaml index fd140f5b4..b205543ef 100644 --- a/workflow/config.yaml +++ b/workflow/config.yaml @@ -223,6 +223,44 @@ clean_ignore_tiles: [] # per-tile, in-job, from the tile's own sexcat). ngmix_chunks: 8 +# COVERAGE MASK (not wired yet — the rules that read this block land in a later +# commit; the keys are parked here so the two numbers below survive the deletion +# of the only two files that ever carried them). +# +# The map counts, per sky pixel, the number of exposures with a VALID PSF MODEL +# — only CCDs whose PSF model was actually written are stamped. In the v1.x +# chain that set was approximated by subtracting a summary-scrape file from all +# 40*N candidates; in the workflow it is exact, read off exp_persist.json's +# files[] (psfex_interp returns WITHOUT writing on NOT_ENOUGH_STARS / BAD_CHI2 / +# FILE_NOT_FOUND). That makes `validation_psf-*.fits` in `persist_exp:` above a +# PRECONDITION: empty that list and no valid-PSF record exists to build from. +# +# nside_coverage / nside: 128 / 131072 is the PRODUCTION pair, and it is not the +# CoverageMapBuilder default (32 / 2048, meant for lighter-weight offline use). +# nside=131072 is ~0.1"/pixel, CHOSEN TO MATCH THE UNIONS BIT-MASK RESOLUTION so +# that coverage and mask align pixel-wise. Inheriting the class defaults yields a +# mask that does not align, and the consumer would not notice. +# +# Consumer: sp_validation applies these healsparse structural masks in +# notebooks/demo_apply_hsp_masks.py (it reads the coverage .hsp and writes the +# masked comprehensive catalogue). +# +# Plot windows are the two UNIONS sky regions, with the colourbar clipped to the +# 1-5 exposure range; plotting stays OUT of the DAG (a human act on a durable +# product), so these are defaults for `plot_coverage_map`, not rule params. +# +# coverage: +# enabled: false +# nside_coverage: 128 +# nside: 131072 +# plot: +# colorbar: true +# n_exp_min: 1 +# n_exp_max: 5 +# regions: +# SGC: {ra_min: -20, ra_max: 45, dec_min: 18, dec_max: 40} +# NGC: {ra_min: 110, ra_max: 270, dec_min: 28, dec_max: 90} + # The container's installed shapepipe is overridden by the prod worktree via # --env PYTHONPATH in profiles/nibi (settled call 3); no config knob here. # build_index.py's missing-tile fraction floor is passed by workflow/bin/sp From 48d1732ada9f973dcedb5276de24a1d025e11257 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 16:16:53 +0200 Subject: [PATCH 06/36] build: guard the container's script-symlink globs against an empty match Both symlink loops (runtime :119-124, dev :170-175) iterate /app/scripts/*/*$ext for ext in .py .sh .bash. scripts/sh/ currently holds exactly seven .bash files and this retirement takes all seven; once the last one goes the glob stays literal, basename '*' '.bash' returns '*', and the build happily creates a junk symlink named '*' in /usr/local/bin instead of failing. `[ -e "$script" ] || continue` makes an empty match a no-op. Lands before any deletion, so no intermediate commit can build the junk link. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 0a549ae8f..fed629849 100644 --- a/Dockerfile +++ b/Dockerfile @@ -118,6 +118,7 @@ RUN chmod -R go+rwX /app && \ uv pip install --no-deps -e . && \ for ext in .py .sh .bash; do \ for script in /app/scripts/*/*$ext; do \ + [ -e "$script" ] || continue; \ link_name=$(basename $script $ext); \ ln -s $script /usr/local/bin/$link_name; \ done; \ @@ -169,6 +170,7 @@ RUN chmod -R go+rwX /app && \ uv pip install --no-deps -e . && \ for ext in .py .sh .bash; do \ for script in /app/scripts/*/*$ext; do \ + [ -e "$script" ] || continue; \ link_name=$(basename $script $ext); \ ln -s $script /usr/local/bin/$link_name; \ done; \ From 864f48666308c82b6a61dabd2633186ae98003ce Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 16:17:56 +0200 Subject: [PATCH 07/36] refactor(merge_headers): drop the pre-workflow single-run-root branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `has_option("EXP_BASE_DIR")` selected between two layouts, not two science modes: the tile branch is the sharded per-exposure store the workflow builds, the else branch the pre-Snakemake layout where a tile run and its exposures shared one run root and the header .npy files arrived directly in input_file_list. The only configs that took the else branch live in example/cfis/defunct/; every live surface (workflow/config/cfis, example/cfis/*canfar*, example/cfis_image_sims) sets EXP_BASE_DIR — checked by grepping every .ini with a [MERGE_HEADERS_RUNNER] section. Tile mode is now unconditional, so a config that omits the key fails loudly with NoOptionError instead of silently merging the wrong file list. The package docstring documents EXP_BASE_DIR and drops OUTPUT_PATH, which the runner never read, and names find_exposures_runner as the parent (it is the exp_numbers file that arrives as input; split_exp_runner's headers are reached through EXP_BASE_DIR). merge_headers()'s tile_number=None default stays — test_sextractor_post_process calls it directly. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- .../modules/merge_headers_package/__init__.py | 11 ++-- src/shapepipe/modules/merge_headers_runner.py | 56 +++++++++---------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/shapepipe/modules/merge_headers_package/__init__.py b/src/shapepipe/modules/merge_headers_package/__init__.py index ed8416790..cd47d6a3d 100644 --- a/src/shapepipe/modules/merge_headers_package/__init__.py +++ b/src/shapepipe/modules/merge_headers_package/__init__.py @@ -4,9 +4,10 @@ :Author: Axel Guinot -:Parent module: ``split_exp_runner`` +:Parent module: ``find_exposures_runner`` -:Input: Numpy binary files (``.npy``) with single-exposure header information +:Input: An ``exp_numbers`` text file listing the tile's exposures; the + per-exposure header files (``.npy``) are found under ``EXP_BASE_DIR`` :Output: Single SQL file with combined header information @@ -20,8 +21,10 @@ Module-specific config file entries =================================== -OUTPUT_PATH : str, optional - Overrides the default module output directory under ``[FILE]:OUTPUT_DIR`` +EXP_BASE_DIR : str + Root of the per-exposure work directories; the header ``.npy`` files are + collected from ``///`` for every exposure + listed in the input ``exp_numbers`` file """ diff --git a/src/shapepipe/modules/merge_headers_runner.py b/src/shapepipe/modules/merge_headers_runner.py index d7b0bd035..21a2ec08b 100644 --- a/src/shapepipe/modules/merge_headers_runner.py +++ b/src/shapepipe/modules/merge_headers_runner.py @@ -34,38 +34,32 @@ def merge_headers_runner( output_dir = run_dirs["output"] w_log.info(f"output_dir = {output_dir}") - if config.has_option(module_config_sec, "EXP_BASE_DIR"): - # Tile-level mode: input is an exp_numbers txt file; collect header - # files from each per-exposure work directory via get_exp_output_files. - exp_base_dir = config.getexpanded(module_config_sec, "EXP_BASE_DIR") + # The input is an exp_numbers txt file (find_exposures_runner); the header + # files themselves are collected from each per-exposure work directory + # under EXP_BASE_DIR via get_exp_output_files. + exp_base_dir = config.getexpanded(module_config_sec, "EXP_BASE_DIR") - # In serial mode several tiles' exp_numbers files can arrive in a - # single call; merge each tile into its own per-tile sqlite file. - for exp_numbers_file in (item[0] for item in input_file_list): - w_log.info( - f"Tile-level merge: collecting headers from {exp_base_dir} " - f"using {exp_numbers_file}" - ) - headers_file_list = get_exp_output_files( - exp_base_dir, - exp_numbers_file, - "split_exp_runner", - "headers", - ".npy", - w_log=w_log, - ) - # Extract tile number from the exp_numbers filename, e.g. - # "exp_numbers-284.272-1.000.txt" -> "-284.272-1.000" - base = os.path.splitext(os.path.basename(exp_numbers_file))[0] - tile_number = re.sub(r"^exp_numbers", "", base) - merge_headers(headers_file_list, output_dir, tile_number) - w_log.info( - f"Merged {len(headers_file_list)} exposure header files" - ) - else: - # Per-exposure mode: input_file_list already contains the header files. - merge_headers(input_file_list, output_dir) - w_log.info(f"Merged {len(input_file_list)} input file headers") + # In serial mode several tiles' exp_numbers files can arrive in a + # single call; merge each tile into its own per-tile sqlite file. + for exp_numbers_file in (item[0] for item in input_file_list): + w_log.info( + f"Collecting headers from {exp_base_dir} " + f"using {exp_numbers_file}" + ) + headers_file_list = get_exp_output_files( + exp_base_dir, + exp_numbers_file, + "split_exp_runner", + "headers", + ".npy", + w_log=w_log, + ) + # Extract tile number from the exp_numbers filename, e.g. + # "exp_numbers-284.272-1.000.txt" -> "-284.272-1.000" + base = os.path.splitext(os.path.basename(exp_numbers_file))[0] + tile_number = re.sub(r"^exp_numbers", "", base) + merge_headers(headers_file_list, output_dir, tile_number) + w_log.info(f"Merged {len(headers_file_list)} exposure header files") # No return objects return None, None From bf223fc1f9fdc6986487fcf720029f2a999f6630 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 16:18:46 +0200 Subject: [PATCH 08/36] refactor(psfex_interp): drop the run-log-resolved multi-epoch branch MULTI-EPOCH mode chose between two ways of finding the psfex_runner output dirs: ME_DOT_PSF_EXP_DIR resolves them under the sharded per-exposure store, the else branch parsed 'last:'/'all:' out of ME_DOT_PSF_DIR and asked log_run_sp.txt which run directory was newest -- the pre-Snakemake single-run-root layout. The only .ini setting ME_DOT_PSF_DIR are example/cfis/defunct/config_tile_PiViSmVi{,_canfar}.ini. Resolving through a run log is exactly what the workflow removed: every rule writes into one fixed store cleared by unit_pre, so "the newest run_sp_* dir" has no meaning. Dropping the branch also drops the now-dead run_log.get_last_dir/get_all_dirs import from this module. run_log.py itself stays -- mccd_interp_runner:45,91 still calls get_last_dir (the MCCD chain is a separate, science-level decision). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- .../modules/psfex_interp_package/__init__.py | 14 ++++-- src/shapepipe/modules/psfex_interp_runner.py | 47 ++++++------------- 2 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/shapepipe/modules/psfex_interp_package/__init__.py b/src/shapepipe/modules/psfex_interp_package/__init__.py index 1c3b51ca4..2b6a161b1 100644 --- a/src/shapepipe/modules/psfex_interp_package/__init__.py +++ b/src/shapepipe/modules/psfex_interp_package/__init__.py @@ -32,14 +32,18 @@ Threshold of stars under which the PSF is not interpolated CHI2_THRESH : int Threshold for chi squared (:math:`\chi^2`) -ME_DOT_PSF_DIR : str - Module name of last run producing PSFEx PSF model files, for multi-epoch - processing. The specifier "last:" is not required +ME_DOT_PSF_EXP_DIR : str + Root of the per-exposure work directories, for multi-epoch processing; + the ``psfex_runner`` output directory of every exposure listed in the + input ``exp_numbers`` file is resolved beneath it ME_DOT_PSF_PATTERN : str Input file name pattern for PSFEx PSF model files, for multi-epoch processing -ME_LOG_WCS : str - Path to world coordinate system log file (``*sqlite``) + +In ``MULTI-EPOCH`` mode the input file list is positional and carries three +entries -- the galaxy catalogue, the world-coordinate-system log +(``*sqlite``) and the ``exp_numbers`` file -- set through ``FILE_PATTERN`` +and ``FILE_EXT``, not through module-specific keys. """ diff --git a/src/shapepipe/modules/psfex_interp_runner.py b/src/shapepipe/modules/psfex_interp_runner.py index 67c21adfc..5627d5d20 100644 --- a/src/shapepipe/modules/psfex_interp_runner.py +++ b/src/shapepipe/modules/psfex_interp_runner.py @@ -10,7 +10,6 @@ from shapepipe.modules.psfex_interp_package import psfex_interp from shapepipe.pipeline.exp_utils import get_exp_output_dirs -from shapepipe.pipeline.run_log import get_last_dir, get_all_dirs @module_runner( @@ -62,38 +61,22 @@ def psfex_interp_runner( # Run in MULTI-EPOCH mode elif mode == "MULTI-EPOCH": - # Fetch multi-epoch parameters - if config.has_option(module_config_sec, "ME_DOT_PSF_EXP_DIR"): - # v2.0: locate psfex_runner output dirs via the $SP_EXP tree - exp_base_dir = config.getexpanded( - module_config_sec, "ME_DOT_PSF_EXP_DIR" - ) - if len(input_file_list) < 3: - raise ValueError( - "ME_DOT_PSF_EXP_DIR requires the exposure-numbers file" - + " as a third input; add 'exp_numbers' to FILE_PATTERN" - + f" and FILE_EXT in the [{module_config_sec}] config" - + " section." - ) - exp_numbers_file = input_file_list[2] - dot_psf_dirs = get_exp_output_dirs( - exp_base_dir, exp_numbers_file, "psfex_runner", w_log - ) - else: - module = config.getexpanded( - module_config_sec, - "ME_DOT_PSF_DIR", + # Fetch multi-epoch parameters. The psfex_runner output dirs are + # located through the per-exposure work tree. + exp_base_dir = config.getexpanded( + module_config_sec, "ME_DOT_PSF_EXP_DIR" + ) + if len(input_file_list) < 3: + raise ValueError( + "ME_DOT_PSF_EXP_DIR requires the exposure-numbers file" + + " as a third input; add 'exp_numbers' to FILE_PATTERN" + + f" and FILE_EXT in the [{module_config_sec}] config" + + " section." ) - module_name = module.split(":")[-1] - if "last" in module: - dot_psf_dirs = [get_last_dir(run_dirs["run_log"], module_name)] - elif "all" in module: - dot_psf_dirs = get_all_dirs(run_dirs["run_log"], module_name) - else: - raise ValueError( - "Expected qualifier 'last:' or 'all' before module" - + f" '{module}' in config entry 'ME_DOT_PSF_DIR'" - ) + exp_numbers_file = input_file_list[2] + dot_psf_dirs = get_exp_output_dirs( + exp_base_dir, exp_numbers_file, "psfex_runner", w_log + ) dot_psf_pattern = config.get( module_config_sec, From 7865ef8efe3057adbc5f8f872888b98c681b9957 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 16:19:21 +0200 Subject: [PATCH 09/36] refactor(vignetmaker): drop the run-log-resolved multi-epoch branch Same shape as psfex_interp: ME_IMAGE_EXP_DIR + ME_IMAGE_EXP_RUNNERS resolve each runner's output dir under the sharded per-exposure store, while the else branch parsed 'last:'/'all:' out of ME_IMAGE_DIR and asked log_run_sp.txt. Its comment was the only place in src/ that said "v1" outright. The only .ini setting ME_IMAGE_DIR are example/cfis/defunct/config_tile_PiViSmVi{,_canfar}.ini. The dropped run_log import leaves get_all_dirs with a single caller in the tree (scripts/python/get_number_objects.py, itself slated for deletion); get_last_dir keeps mccd_interp_runner, so run_log.py stays. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- .../modules/vignetmaker_package/__init__.py | 19 +++++-- src/shapepipe/modules/vignetmaker_runner.py | 56 +++++++------------ 2 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/shapepipe/modules/vignetmaker_package/__init__.py b/src/shapepipe/modules/vignetmaker_package/__init__.py index b24f8397e..27c679c23 100644 --- a/src/shapepipe/modules/vignetmaker_package/__init__.py +++ b/src/shapepipe/modules/vignetmaker_package/__init__.py @@ -35,15 +35,22 @@ Run mode for module, options are ``CLASSIC`` or ``MULTI-EPOCH`` PREFIX : str or list Output file name prefix(es) -ME_IMAGE_DIR : list - Module names of last run producing single-exposure flags, images, weights, - and SExtractor background images, for multi-epoch processing. The specifier - "last:" is not required +ME_IMAGE_EXP_DIR : str + Root of the per-exposure work directories, for multi-epoch processing; + each runner output directory is resolved beneath it for every exposure + listed in the input ``exp_numbers`` file +ME_IMAGE_EXP_RUNNERS : list + Names of the runners producing the single-exposure flags, images, weights + and SExtractor background images, in the same order as + ``ME_IMAGE_PATTERN`` ME_IMAGE_PATTERN : list Input file name patterns for flag, image, weight, and SExtractor background files, for multi-epoch processing -ME_LOG_WCS : str - Path to world coordinate system log file (``*sqlite``) + +In ``MULTI-EPOCH`` mode the input file list is positional and carries three +entries -- the galaxy catalogue, the world-coordinate-system log +(``*sqlite``) and the ``exp_numbers`` file -- set through ``FILE_PATTERN`` +and ``FILE_EXT``, not through module-specific keys. """ diff --git a/src/shapepipe/modules/vignetmaker_runner.py b/src/shapepipe/modules/vignetmaker_runner.py index 2d9661faa..bad5a22df 100644 --- a/src/shapepipe/modules/vignetmaker_runner.py +++ b/src/shapepipe/modules/vignetmaker_runner.py @@ -10,7 +10,6 @@ from shapepipe.modules.vignetmaker_package import vignetmaker as vm from shapepipe.pipeline.exp_utils import get_exp_output_dirs -from shapepipe.pipeline.run_log import get_last_dir, get_all_dirs @module_runner( @@ -105,44 +104,27 @@ def vignetmaker_runner( elif mode == "MULTI-EPOCH": # Multi-epoch exposures - if config.has_option(module_config_sec, "ME_IMAGE_EXP_DIR"): - # v2.0: locate runner output dirs via the $SP_EXP tree - exp_base_dir = config.getexpanded( - module_config_sec, "ME_IMAGE_EXP_DIR" + # Locate the runner output dirs through the per-exposure work tree. + exp_base_dir = config.getexpanded( + module_config_sec, "ME_IMAGE_EXP_DIR" + ) + if len(input_file_list) < 3: + raise ValueError( + "ME_IMAGE_EXP_DIR requires the exposure-numbers" + + " file as a third input; add 'exp_numbers' to" + + " FILE_PATTERN and FILE_EXT in the" + + f" [{module_config_sec}] config section." ) - if len(input_file_list) < 3: - raise ValueError( - "ME_IMAGE_EXP_DIR requires the exposure-numbers" - + " file as a third input; add 'exp_numbers' to" - + " FILE_PATTERN and FILE_EXT in the" - + f" [{module_config_sec}] config section." - ) - exp_numbers_file = input_file_list[2] - exp_runner_names = config.getlist( - module_config_sec, "ME_IMAGE_EXP_RUNNERS" + exp_numbers_file = input_file_list[2] + exp_runner_names = config.getlist( + module_config_sec, "ME_IMAGE_EXP_RUNNERS" + ) + image_dirs = [] + for runner_name in exp_runner_names: + dirs = get_exp_output_dirs( + exp_base_dir, exp_numbers_file, runner_name, w_log ) - image_dirs = [] - for runner_name in exp_runner_names: - dirs = get_exp_output_dirs( - exp_base_dir, exp_numbers_file, runner_name, w_log - ) - image_dirs.append(dirs) - else: - # v1: run-log based lookup via symlinked exposure run dirs - modules = config.getlist(module_config_sec, "ME_IMAGE_DIR") - image_dirs = [] - for module in modules: - module_name = module.split(":")[-1] - if "last" in module: - dirs = [get_last_dir(run_dirs["run_log"], module_name)] - elif "all" in module: - dirs = get_all_dirs(run_dirs["run_log"], module_name) - else: - raise ValueError( - "Expected qualifier 'last:' or 'all' before module" - + f" '{module}' in config entry 'ME_IMAGE_DIR'" - ) - image_dirs.append(dirs) + image_dirs.append(dirs) image_pattern = config.getlist( module_config_sec, From 2112fba92a6b46aaac98a78cab086fbdf70f50b1 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 16:20:04 +0200 Subject: [PATCH 10/36] docs(exp_utils): name the workflow, not the bash generation it came from Three naming-only strings: ":4 In the v2.0 pipeline", ":43 the example path /arc/home/kilbinger/v2.0/exp", and ":94 "mirrors run_job_canfar_v2.0.sh"" -- a script that no longer exists. The last one is the load-bearing one: it is the only comment explaining WHY the 2-char shard exists, so it now points at the thing that actually builds that tree, rule tile_exp_forest / workflow/scripts/build_forest.py. The globs themselves are untouched -- tile.smk:11-13 and build_forest.py:8-11 depend on exactly this layout. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- src/shapepipe/pipeline/exp_utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/shapepipe/pipeline/exp_utils.py b/src/shapepipe/pipeline/exp_utils.py index b934f0bb3..889ff31c1 100644 --- a/src/shapepipe/pipeline/exp_utils.py +++ b/src/shapepipe/pipeline/exp_utils.py @@ -1,11 +1,10 @@ """EXPOSURE UTILITIES. Utility functions for accessing per-exposure runner outputs from the -tile level. In the v2.0 pipeline, each single exposure is processed in -its own work directory ``///``. Tile-level -modules that need files produced by a per-exposure runner use the -functions here to discover those files by scanning the contributing -exposure directories. +tile level. Each single exposure is processed in its own work directory +``///``. Tile-level modules that need files +produced by a per-exposure runner use the functions here to discover those +files by scanning the contributing exposure directories. :Author: Martin Kilbinger @@ -40,7 +39,7 @@ def get_exp_output_files( ---------- exp_base_dir : str Root directory that contains all per-exposure work directories, - e.g. ``/arc/home/kilbinger/v2.0/exp`` + e.g. ``/exp`` exp_numbers_file : str Path to a text file listing one exposure ID per line, e.g. ``exp_numbers-301-279.txt`` @@ -91,7 +90,8 @@ def get_exp_output_files( missing = [] for exp_id in exp_ids: - # Directory structure mirrors run_job_canfar_v2.0.sh: + # Directory structure mirrors the workflow's per-exposure store, built + # by rule tile_exp_forest / workflow/scripts/build_forest.py: # exp_prefix = first 2 chars of exp_id (e.g. "21") # exp_base = exp_id without trailing letter if present (e.g. "2113864"), # or full exp_id for numeric-only ids (image sims) From cc55734f4be1f71b9c4786f7dd1c5b0e68d0b906 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 16:21:03 +0200 Subject: [PATCH 11/36] config: drop the last version labels and a dead line from the live configs Four comment rewords -- "for [the] v2.0 per-exposure pipeline" -> "for the per-exposure pipeline" in config_tile_PiViVi.ini (workflow and both example/cfis canfar variants) and example/cfis_image_sims'. There is one per-exposure pipeline; naming it after a bash generation only dates it. Also: delete the commented-out CHECK_EXISTING_DIR pointing at a run_sp_Gie_prev dir the workflow never creates (unit_pre clears each stage's own dir), and drop the catalogue-version suffix from example/cfis/config_valjoint_Pl_mccd.ini's FILE_PATTERN (unions_shapepipe_psf_2022_v1.0.2 -> unions_shapepipe_psf_2022). Safe mid-campaign: .ini content is not hashed into any params -- only workflow/scripts/*.py are (Snakefile:316-353) -- and rules name configs by path in their shell string, so a comment edit is not a rerun trigger. Verified none of these files is one of the eleven workflow/config/cfis -> example/cfis symlink targets, and all eleven still resolve. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- example/cfis/config_tile_PiViVi_canfar_sx.ini | 4 ++-- example/cfis/config_tile_PiViVi_canfar_uc.ini | 4 ++-- example/cfis/config_valjoint_Pl_mccd.ini | 2 +- example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini | 2 +- workflow/config/cfis/config_exp_Gie.ini | 2 -- workflow/config/cfis/config_tile_PiViVi.ini | 4 ++-- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/example/cfis/config_tile_PiViVi_canfar_sx.ini b/example/cfis/config_tile_PiViVi_canfar_sx.ini index 71e4b17e3..8266ad03e 100644 --- a/example/cfis/config_tile_PiViVi_canfar_sx.ini +++ b/example/cfis/config_tile_PiViVi_canfar_sx.ini @@ -87,7 +87,7 @@ CHI2_THRESH = 2 # Multi-epoch mode parameters # Root directory of per-exposure work directories; replaces ME_DOT_PSF_DIR -# for v2.0 per-exposure pipeline. psfex_runner/output/ dirs are discovered +# for the per-exposure pipeline. psfex_runner/output/ dirs are discovered # by scanning $SP_EXP for the exposures listed in the exp_numbers input file. ME_DOT_PSF_EXP_DIR = $SP_EXP @@ -162,7 +162,7 @@ PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure # run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the v2.0 per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner, sextractor_runner, sextractor_runner ME_IMAGE_PATTERN = flag, image, weight, background, background_rms diff --git a/example/cfis/config_tile_PiViVi_canfar_uc.ini b/example/cfis/config_tile_PiViVi_canfar_uc.ini index b33361b6d..63cd7beee 100644 --- a/example/cfis/config_tile_PiViVi_canfar_uc.ini +++ b/example/cfis/config_tile_PiViVi_canfar_uc.ini @@ -85,7 +85,7 @@ CHI2_THRESH = 2 # Multi-epoch mode parameters # Root directory of per-exposure work directories; replaces ME_DOT_PSF_DIR -# for v2.0 per-exposure pipeline. psfex_runner/output/ dirs are discovered +# for the per-exposure pipeline. psfex_runner/output/ dirs are discovered # by scanning $SP_EXP for the exposures listed in the exp_numbers input file. ME_DOT_PSF_EXP_DIR = $SP_EXP @@ -160,7 +160,7 @@ PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure # run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the v2.0 per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner, sextractor_runner, sextractor_runner ME_IMAGE_PATTERN = flag, image, weight, background, background_rms diff --git a/example/cfis/config_valjoint_Pl_mccd.ini b/example/cfis/config_valjoint_Pl_mccd.ini index 086690815..81c444b55 100644 --- a/example/cfis/config_valjoint_Pl_mccd.ini +++ b/example/cfis/config_valjoint_Pl_mccd.ini @@ -39,7 +39,7 @@ INPUT_DIR = ./SP OUTPUT_DIR = ./output # FILE_PATTERN (opional) list of string patterns to identify input files -FILE_PATTERN = unions_shapepipe_psf_2022_v1.0.2 +FILE_PATTERN = unions_shapepipe_psf_2022 # FILE_EXT (opional) list of string extensions to identify input files FILE_EXT = .fits diff --git a/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini b/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini index 28ed565af..592778bef 100644 --- a/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini +++ b/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini @@ -121,7 +121,7 @@ PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure # run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the v2.0 per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner ME_IMAGE_PATTERN = flag, image, weight diff --git a/workflow/config/cfis/config_exp_Gie.ini b/workflow/config/cfis/config_exp_Gie.ini index 7c6297cf0..0c17cf19f 100644 --- a/workflow/config/cfis/config_exp_Gie.ini +++ b/workflow/config/cfis/config_exp_Gie.ini @@ -95,5 +95,3 @@ N_TRY = 3 # Retrieve command options, optional RETRIEVE_OPTIONS = --certfile=$HOME/.ssl/cadcproxy.pem - -#CHECK_EXISTING_DIR = $SP_RUN/output/run_sp_Gie_prev diff --git a/workflow/config/cfis/config_tile_PiViVi.ini b/workflow/config/cfis/config_tile_PiViVi.ini index 2e1547f3e..6e9390734 100644 --- a/workflow/config/cfis/config_tile_PiViVi.ini +++ b/workflow/config/cfis/config_tile_PiViVi.ini @@ -107,7 +107,7 @@ CHI2_THRESH = 2 # Multi-epoch mode parameters # Root directory of per-exposure work directories; replaces ME_DOT_PSF_DIR -# for v2.0 per-exposure pipeline. psfex_runner/output/ dirs are discovered +# for the per-exposure pipeline. psfex_runner/output/ dirs are discovered # by scanning $SP_EXP for the exposures listed in the exp_numbers input file. ME_DOT_PSF_EXP_DIR = $SP_EXP @@ -182,7 +182,7 @@ PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure # run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the v2.0 per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner, sextractor_runner, sextractor_runner ME_IMAGE_PATTERN = flag, image, weight, background, background_rms From c91d61ab8212dbb0311b7a5f8db683fbd74deec9 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 17:38:12 +0200 Subject: [PATCH 12/36] feat(workflow): record each exposure's per-CCD sky footprint in the DAG New localrule `exp_footprint`, one JSON per exposure on the persistent root giving the four sky corners of every CCD that got a PSF model. It is the raw material of the campaign coverage mask, and it replaces the v1.x chain's two approximations with exact local reads of records the workflow already writes: * WHICH CCDs have a PSF <- exp_persist.json's `files[].name`. Exact, because psfex_interp returns without writing validation_psf-*.fits on NOT_ENOUGH_STARS / BAD_CHI2 / FILE_NOT_FOUND. The v1.x tool subtracted a summary-scrape file from all 40*N candidates. * WCS <- headers-.npy, written by exp_split. The v1.x tool downloaded ~25k exposure headers from VOSpace. Requested by `rule all` directly and NOT gated on `coverage:`, for the reason persist_targets is: the WCS array is on /scratch and the purge takes it, so deferring the record until someone wants a map means the map can never be built. Tombstone-excluded exactly as persist_targets is, and clean_exposure takes the manifest as an input so reclamation cannot overtake the read. The rule declares only the durable persist manifest, deliberately not exp_psf's as well: exp_persist already orders it after the whole PSF chain, and a scratch manifest in the input list of a durable-output rule would schedule a four-hour VOS rebuild to satisfy a few KB of provenance. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- workflow/Snakefile | 48 ++++++- workflow/rules/exposure.smk | 79 ++++++++++-- workflow/scripts/exp_footprint.py | 203 ++++++++++++++++++++++++++++++ 3 files changed, 319 insertions(+), 11 deletions(-) create mode 100644 workflow/scripts/exp_footprint.py diff --git a/workflow/Snakefile b/workflow/Snakefile index 81e395951..0ddaacec4 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -37,6 +37,7 @@ import json import os import sqlite3 import sys +from fnmatch import fnmatch from pathlib import Path from snakemake.exceptions import WorkflowError @@ -323,6 +324,7 @@ FOREST_HASH = script_hash("build_forest.py") CLEAN_HASH = script_hash("clean_exposure.py") CLEAN_TILE_HASH = script_hash("clean_tile.py") PERSIST_HASH = script_hash("persist_exp.py") +FOOTPRINT_HASH = script_hash("exp_footprint.py") # Same argument for star_cats.py, which both star-cat rules call: their params # otherwise fingerprint nothing but paths, so an edit to the chunking or the cut # would never rerun them. ONE hash for both rules because it is one script — and @@ -482,6 +484,46 @@ def persist_targets(): return sorted(prod_exp_manifest(e, "exp_persist") for e in exps if not Path(tombstone(e)).exists()) +# --- per-CCD sky footprints (the coverage map's raw material) --------------- +# `exp_footprint` reads the valid-PSF CCD set off exp_persist.json's MEMBERS, so +# `persist_exp:` naming the psfex_interp product is that rule's PRECONDITION, not +# a preference: with those files unpacked the manifest carries no valid-PSF set +# and there is nothing to build a footprint from. Asked by matching a +# hypothetical member name rather than by string-matching the pattern, so any +# spelling that would have packed them counts (exp_footprint.py asks the same +# question of the manifest it actually reads). +# +# Absent the pattern, the footprint rule is simply OFF — no jobs, no error. It is +# `coverage:` being on that turns the same missing pattern into a parse-time +# failure (coverage.smk), because there the user asked for a map. +PSF_PRODUCT_PROBE = "validation_psf-0000000-0.fits" +PERSIST_HAS_PSF = any(fnmatch(PSF_PRODUCT_PROBE, pat) for pat in PERSIST_EXP) + + +def footprint_targets(): + """Which exposures this invocation must record a per-CCD sky footprint for. + + REQUESTED BY `rule all` DIRECTLY, AND NOT GATED ON `coverage:`. The record is + derived from `headers-.npy`, which lives on /scratch and which the + 60-day purge takes whether or not this campaign ever builds a map — so + building it only when a map is asked for means that turning coverage on a + month later finds the WCS gone and no way back short of re-downloading the + exposures. This is persist_targets' argument above, applied to the other + scratch-only input coverage needs: milliseconds and a few KB now, against an + irrecoverable loss later. + + Scope, and the tombstone exclusion, are persist_targets' exactly: a cleaned + exposure's footprint was written before its store went (clean_exposure takes + this manifest as an input), and asking again would rebuild the chain from VOS. + + HEAD PROCESS ONLY, for the same reason as clean_targets() above. + """ + if not PERSIST_HAS_PSF or not workflow.is_main_process: + return [] + exps = {e for t in TILES_READY for e in tile_exposures(t)} + return sorted(prod_exp_manifest(e, "exp_footprint") for e in exps + if not Path(tombstone(e)).exists()) + # --- tile reclamation (D5) -------------------------------------------------- # A SEPARATE FLAG from `clean:`, deliberately (config.yaml carries the full # argument): exposure reclamation costs nothing but a rebuild if a tile is @@ -647,19 +689,21 @@ include: "rules/tile.smk" # # exp_persist joins them for the same arithmetic — a few MB of `tar` per exposure, # ~20k of them at DR6 scale, each far shorter than the scheduling latency that -# would submit it (exposure.smk argues the placement in full). +# would submit it (exposure.smk argues the placement in full). exp_footprint sits +# beside it and is smaller still: one pickle load and ~160 pixel_to_world calls. # # star_catalogue and exp_star_cat are MID-CHAIN localrules, so they must stay out # of any future `group:` label: a local job cannot be fused into a submitted group. # exp_persist sits between exp_psf and clean_exposure, both of which are outside # every group already (exp_psf is heavy, clean_exposure is local), so it adds no # new constraint. The two clean rules are DAG leaves and have none either. -localrules: all, prepare_all_tiles, clean_exposure, clean_tile, star_catalogue, exp_star_cat, exp_persist +localrules: all, prepare_all_tiles, clean_exposure, clean_tile, star_catalogue, exp_star_cat, exp_persist, exp_footprint rule all: input: [final_cat(t) for t in TILES_READY], persist_targets(), + footprint_targets(), clean_targets(), clean_tile_targets(), diff --git a/workflow/rules/exposure.smk b/workflow/rules/exposure.smk index d15bf64fa..511d983d0 100644 --- a/workflow/rules/exposure.smk +++ b/workflow/rules/exposure.smk @@ -1,8 +1,8 @@ """Exposure chain — per exposure, keyed by exp base id (dedup is structural). - exp_get_images -> exp_split -----> exp_mask -> exp_psf -> exp_persist - -> exp_star_cat --/ - star_catalogue ---------------/ + exp_get_images -> exp_split ---> exp_mask -> exp_psf -> exp_persist -> exp_footprint + -> exp_star_cat -/ + star_catalogue -------------/ ``star_catalogue`` is campaign-level, not per-exposure: one fetch of the whole footprint's stars, which every exposure's ``exp_star_cat`` then cuts locally. @@ -12,12 +12,13 @@ reads fixed ``$SP_RUN/output/run_sp_exp_*`` INPUT_DIRs, so nothing resolves a run log. There is no `prepare_exposures` aggregation target: these chains hang off the compute DAG (`all` <- final_cat <- tile chain <- exposure manifests). -``exp_persist`` is the one rule here that writes to the PERSISTENT root: it -packs the PSF products named by `persist_exp:` into one tar per exposure off -/scratch before the purge (or clean_exposure) can take them. It is a separate +The last two rules write to the PERSISTENT root, and both exist because the +exposure store is on /scratch and the purge takes it. ``exp_persist`` packs the +PSF products named by `persist_exp:` into one tar per exposure; it is a separate rule from exp_psf precisely so that editing that list costs a `tar` and not a -four-hour refit; the full -argument is in workflow/scripts/persist_exp.py. +four-hour refit (workflow/scripts/persist_exp.py). ``exp_footprint`` then records +where on the sky the CCDs that got a PSF model are, which is the coverage mask's +raw material (workflow/scripts/exp_footprint.py, rules/coverage.smk). NO temp() anywhere in this file, ever (D5). Exposures overlap tiles by construction (~7-10 tiles each), so their consumer set closes over the CAMPAIGN, @@ -375,6 +376,58 @@ rule exp_persist: " {params.patterns}" +# --- per-CCD sky footprints ------------------------------------------------- +# One JSON per exposure recording, for each CCD that HAS a PSF model, the four +# sky corners of that CCD. It is the raw material of the campaign's coverage mask +# (rules/coverage.smk), and it replaces the v1.x chain's VOSpace header download +# + summary-scrape subtraction with a local read of two things this workflow +# already wrote. workflow/scripts/exp_footprint.py argues both inputs and the +# CCD-index invariant tests/unit/test_exp_footprint.py pins. +# +# A LOCALRULE (declared in the Snakefile), by exp_persist's arithmetic and then +# some: one pickle load and ~160 pixel_to_world calls, milliseconds, against a +# scheduling latency of seconds and ~20k exposures at DR6 scale. +# +# ONE DECLARED INPUT, AND IT IS THE PERSIST MANIFEST — deliberately NOT +# exp_psf's as well, even though the WCS array this rule reads is written by +# exp_split and lives in the same scratch store. exp_persist already orders this +# rule after the whole PSF chain, so the second edge would buy no ordering; what +# it WOULD buy is a scratch manifest (the one clean_exposure deletes) in the +# input list of a rule whose output is durable. A persistent-root manifest +# outliving a purged scratch store is a real state, and in it that edge would +# schedule a four-hour VOS rebuild of the exposure to satisfy a few KB of +# provenance. Declaring only the durable input makes the same state a loud +# one-line failure from the script instead. +# +# WRITES TO THE PERSISTENT ROOT, beside exp_persist's manifest and for the same +# reason: the record must outlive both reclamation and the purge — a coverage +# map is campaign-cumulative, so a record written today is read by every map +# built after it. +rule exp_footprint: + input: + lambda wc: prod_exp_manifest(wc.exp, "exp_persist") + output: + manifest = f"{PROD_EXP_DIR}/manifests/exp_footprint.json" + # No `log:`, for exp_persist's reason: the failure modes are "no WCS array" + # and "the two stages disagree about the focal plane", both of which the + # script reports on stderr and neither of which has a per-CCD verdict. + params: + exp_dir = lambda wc: exp_dir(wc.exp), + persist = lambda wc: prod_exp_manifest(wc.exp, "exp_persist"), + script_hash = FOOTPRINT_HASH + threads: 1 + retries: 2 + resources: + mem_mb = 2000, + runtime = 10 + shell: + "set -euo pipefail\n" + f"python {SCRIPTS}/exp_footprint.py" + " --exp-dir '{params.exp_dir}' --exp {wildcards.exp}" + " --persist-manifest '{params.persist}'" + " --manifest {output.manifest}" + + # --- reclamation (D5) ------------------------------------------------------- # The one exception to "no reclamation in this file": clean_exposure OWNS # exposure-level deletion, and it is a real job, not temp() bookkeeping, because @@ -415,7 +468,15 @@ rule clean_exposure: # nothing" is a coherent instruction and must not become a dependency on # a rule that would fail for having nothing to copy. lambda wc: ([prod_exp_manifest(wc.exp, "exp_persist")] - if PERSIST_EXP else []) + if PERSIST_EXP else []), + # And the footprint, for the same ordering reason one layer further out: + # it is derived from headers-.npy, which lives in the store this job + # deletes. Reclamation must not overtake the read, and unlike the purge + # this deletion is ours to order. Conditional on the keep list naming the + # PSF products (Snakefile's PERSIST_HAS_PSF) because that is exactly when + # the footprint rule exists at all. + lambda wc: ([prod_exp_manifest(wc.exp, "exp_footprint")] + if PERSIST_HAS_PSF else []) output: tombstone = f"{EXP_DIR}/cleaned.json" params: diff --git a/workflow/scripts/exp_footprint.py b/workflow/scripts/exp_footprint.py new file mode 100644 index 000000000..305a72aaf --- /dev/null +++ b/workflow/scripts/exp_footprint.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 +"""Record ONE exposure's per-CCD sky footprint, for the CCDs that have a PSF model. + +Run as the shell of the in-DAG ``exp_footprint`` rule, never by hand. + +WHAT THIS REPLACES. The v1.x coverage chain answered "which CCDs have a valid +PSF, and where are they on the sky" by downloading ~25k exposure headers from +VOSpace (``header_downloader.py``), scraping a finished campaign's patch +directories for a ``missing_job_32_all.txt`` and SUBTRACTING it from all 40*N +candidates (``ccd_psf_handler.py``), then appending one row per CCD to a shared +``exp_ra_dec.txt``. Every one of those three moves exists because the pre- +Snakemake pipeline kept no per-unit record. The workflow keeps two, both already +on disk, so this rule is a local read of things it produced itself. + +THE TWO INPUTS, AND WHY EACH IS THE RIGHT ONE. + +1. WHICH CCDS HAVE A PSF -> ``exp_persist.json``'s ``files[].name``, the + ``validation_psf--.fits`` members of the persisted tar. This is + EXACT, not inferred: ``psfex_interp`` returns WITHOUT writing that file on + NOT_ENOUGH_STARS, BAD_CHI2 and FILE_NOT_FOUND, and writes it on success, so + the member list IS the valid-PSF set. It is also DURABLE (persistent root, + survives ``clean_exposure``) and a DECLARED RULE OUTPUT, so the DAG orders + this rule after it for free. The alternative considered and rejected was + ``exp_psf.json``'s psfex_interp entry: that is a COUNT, not a list of names, + and it lives inside the directory ``clean_exposure`` deletes wholesale. + +2. WCS -> ``headers-.npy``, written by ``split_exp`` beside the per-CCD + images. A length-N ``dtype=object`` array; element ``i`` is + ``{"WCS": WCS(h), "header": h.tostring()}``, loaded with ``allow_pickle=True`` + exactly as ``merge_headers.py`` does. + +THE LOAD-BEARING INVARIANT, PINNED BY tests/unit/test_exp_footprint.py: +array index ``i`` == the CCD index in ``image--.fits`` == ``-`` +== ``validation_psf--.fits``. ``split_exp`` writes both the image and the +array element from the same ``idx-1`` in one loop, so the alignment is +structural — but it is a cross-component contract (split_exp's numbering vs. +psfex_interp's filenames vs. this record's ids), and nothing else asserts it now +that the old ``summary.get_all_shdus`` test is gone. No index is ever re-derived +here: a CCD's id comes from its position in the array, never from parsing a name. + +WHY THE SHAPE COMES FROM THE STORED HEADER AND NOT FROM THE WCS. astropy hands +back the DECOMPRESSED header for a tile-compressed HDU, so this npy carries true +``NAXIS1/2`` and no ``ZIMAGE`` — while the old VOSpace text headers carried the +binary-table ``NAXIS`` and needed ``ZNAXIS1/2``. ``_image_shape`` handles both, +and is imported rather than reimplemented for exactly that reason. ``WCS`` drops +the ``Z*`` keywords, so it cannot answer the question either way. + +DATA AND MANIFEST IN ONE FILE. 40 rows of 8 floats is a few KB, and inodes are +what bind on /project (persist_exp.py argues the quota arithmetic). Per-exposure +JSON, never an appended shared text file: a single appended file is precisely +what cannot survive 20k parallel jobs, and it is what the v1.x chain used. +``ccds_no_psf`` is carried explicitly so the record is self-describing about +attrition — a reader can tell "this CCD is not in the map" from "this CCD was +never looked at". + +Written byte-stable, no timestamp, tmp-then-``cmp``-then-``mv``, exactly as +``persist_exp.py`` does and for the same reason: mtime is a rerun trigger, and an +unconditional rewrite would make ``clean_exposure`` look out of date once per +invocation. +""" + +import argparse +import filecmp +import json +import sys +from fnmatch import fnmatch +from pathlib import Path + +import numpy as np +from astropy.io.fits import Header + +from shapepipe.utilities.field_corners_extractor import _ccd_corners, _image_shape + +# The split stage's run dir (RUN_NAME in config_exp_Sp.ini) and its module +# output dir. Hardcoded rather than passed, for persist_exp.py's reason: this +# rule reads the split stage's headers and nothing else, and a knob here would +# be a knob for "read some other stage". +HEADERS_DIR = "output/run_sp_exp_Sp/split_exp_runner/output" + +# The name psfex_interp gives a CCD's PSF model at the validation positions. +# Only the PREFIX is ours to know; the rest of the name is the unit id and the +# CCD index, which is what makes the set a set of indices. +PSF_PREFIX = "validation_psf" + + +def valid_ccds(manifest, exp): + """The CCD indices with a PSF model, read off an ``exp_persist`` manifest. + + Raises if the manifest was written by a keep list that never asked for the + PSF files: an empty answer would then be indistinguishable from an exposure + that genuinely lost every CCD, and the coverage map would silently lose a + whole exposure. The Snakefile guards the same precondition at parse time; + this is the per-unit half, and it is the one that sees the manifest. + """ + body = json.loads(Path(manifest).read_text()) + patterns = body.get("patterns") or [] + # Does the keep list ask for these files AT ALL? Asked by matching a + # hypothetical member name rather than by string-matching the pattern, so + # any spelling that would have packed them counts. + probe = f"{PSF_PREFIX}-{exp}-0.fits" + if not any(fnmatch(probe, pat) for pat in patterns): + sys.exit( + f"exp_footprint: {exp}: {manifest} was written by a keep list " + f"({patterns}) that packs no {PSF_PREFIX} files, so it names no " + f"valid-PSF set. Add a pattern matching {probe} to `persist_exp:` " + f"in config.yaml and rerun exp_persist.") + + ccds = set() + for entry in body.get("files") or []: + name = entry.get("name", "") + if not name.startswith(f"{PSF_PREFIX}-{exp}-"): + continue + stem = name[len(f"{PSF_PREFIX}-{exp}-"):].removesuffix(".fits") + if not stem.isdigit(): + sys.exit(f"exp_footprint: {exp}: cannot read a CCD index out of " + f"tar member {name!r}") + ccds.add(int(stem)) + return ccds + + +def footprint(headers, exp, with_psf): + """One record's ``ccds`` and ``ccds_no_psf``, in array order. + + ``headers`` is the ``headers-.npy`` array; index ``i`` IS the CCD + index (see the module docstring). Corners are computed only for the CCDs in + ``with_psf`` — a CCD with no PSF model contributes nothing to a map that + counts exposures with a valid PSF, and computing its corners anyway would + invite a later reader to use them. + """ + ccds, no_psf = [], [] + for i, entry in enumerate(headers): + ccd_id = f"{exp}-{i}" + if i not in with_psf: + no_psf.append(ccd_id) + continue + # The WCS is the pickled object split_exp built; the SHAPE must come + # from the stored header text (module docstring). + shape = _image_shape(Header.fromstring(entry["header"])) + ra, dec = _ccd_corners(entry["WCS"], shape) + ccds.append({"id": ccd_id, "ra": ra, "dec": dec}) + return ccds, no_psf + + +def write_stable(path, body): + """Write JSON tmp-then-``cmp``-then-``mv``; an unchanged body keeps its mtime.""" + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_name(path.name + ".tmp") + try: + tmp.write_text(json.dumps(body, indent=2, sort_keys=True) + "\n") + if path.exists() and filecmp.cmp(tmp, path, shallow=False): + tmp.unlink() # unchanged: leave the mtime alone + else: + tmp.replace(path) + finally: + tmp.unlink(missing_ok=True) + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--exp-dir", required=True, type=Path, + help="the exposure's scratch store") + p.add_argument("--exp", required=True) + p.add_argument("--persist-manifest", required=True, type=Path, + help="/exp///manifests/" + "exp_persist.json; names the valid-PSF CCDs") + p.add_argument("--manifest", required=True, type=Path) + args = p.parse_args() + + npy = args.exp_dir / HEADERS_DIR / f"headers-{args.exp}.npy" + if not npy.exists(): + sys.exit(f"exp_footprint: {args.exp}: no WCS array at {npy} — the " + f"split stage's store is gone or never wrote one " + f"(config_exp_Sp.ini's OUTPUT_SUFFIX must include `image`).") + headers = np.load(npy, allow_pickle=True) + + with_psf = valid_ccds(args.persist_manifest, args.exp) + # A PSF file for a CCD the split never produced means the two stages + # disagree about the focal plane — the one failure the id alignment cannot + # absorb, so it is loud rather than silently dropped. + stray = sorted(i for i in with_psf if i >= len(headers)) + if stray: + sys.exit(f"exp_footprint: {args.exp}: {args.persist_manifest} names " + f"CCD(s) {stray} but {npy} holds only {len(headers)}") + + ccds, no_psf = footprint(headers, args.exp, with_psf) + + write_stable(args.manifest, { + "stage": "exp_footprint", "level": "exp", "unit": args.exp, + "status": "complete", + "source_manifest": str(args.persist_manifest), + "n_ccd_headers": len(headers), + "n_valid_psf": len(ccds), + "ccds": ccds, + "ccds_no_psf": no_psf, + }) + + warn = f" ({len(no_psf)} without a PSF model)" if no_psf else "" + print(f"[exp_footprint] {args.exp}: {len(ccds)}/{len(headers)} CCD " + f"footprint(s){warn} -> {args.manifest}") + + +if __name__ == "__main__": + main() From 38a384ce373498b6fe56b58181ba47fc77a484c5 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 17:44:01 +0200 Subject: [PATCH 13/36] test(workflow): pin the CCD-index contract exp_footprint rests on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit headers-.npy names a CCD by its POSITION in the array; exp_persist.json names it inside validation_psf--.fits. exp_footprint joins the two and nothing cross-checks them, so a permutation would produce a coverage map with the right pixels and the wrong exposure counts, silently. Six synthetic CCDs with distinct WCS centres, three of them PSF-bearing and non-adjacent, so an off-by-one or a "renumber the survivors" bug fails. This test inherits the role of test_coverage.py::test_get_all_shdus, which pinned the same - format against the summary scrape being retired. Also pinned: the record is raw sky across RA=0 (unwrapping belongs to coverage_map_builder, once, at stamping time), a rerun over an unchanged store does not move the manifest's mtime, and the three ways the join can be wrong — a keep list packing no PSF files, a PSF for a CCD the split never wrote, and a persist manifest outliving its scratch store — are loud rather than empty. Corners are cast to plain floats on the way into the record: _ccd_corners hands back numpy scalars, and byte-stability should not rest on json's handling of a float subclass. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- tests/unit/test_exp_footprint.py | 242 ++++++++++++++++++++++++++++++ workflow/scripts/exp_footprint.py | 7 +- 2 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_exp_footprint.py diff --git a/tests/unit/test_exp_footprint.py b/tests/unit/test_exp_footprint.py new file mode 100644 index 000000000..ddd244a6a --- /dev/null +++ b/tests/unit/test_exp_footprint.py @@ -0,0 +1,242 @@ +"""The exp_footprint record, and the CCD-index contract it rests on. + +``workflow/scripts/exp_footprint.py`` joins two records that name CCDs in two +different ways and never cross-check each other: + + * ``headers-.npy`` names a CCD by its POSITION in the array — the same + ``idx-1`` split_exp used for ``image--.fits``; + * ``exp_persist.json`` names a CCD inside a FILENAME, + ``validation_psf--.fits``, written by psfex_interp. + +The whole design depends on those being the same integer, and no code asserts +it: split_exp writes the image and the array element in one loop, psfex_interp +inherits the numbering through the file handler, and the coverage map would be +silently WRONG — right pixels, wrong exposure count — if they ever diverged by a +permutation. This module is where that contract is pinned. It inherits the role +of ``tests/module/test_coverage.py::test_get_all_shdus``, which pinned the same +``-`` format against the summary scrape that is being retired. + +The fixtures are real astropy WCSs, one per CCD with a DISTINCT centre, so a +permutation of the array shows up as corners on the wrong ``id`` rather than as +a passing test. One CCD straddles RA=0 deliberately: the record is raw sky, and +the seam is the map builder's business (``unwrap_ra``), not this script's. + +Deliberately not a module test: nothing here runs shapepipe, only its two +geometry helpers, so it belongs with the fast structural suite. +""" + +import importlib.util +import json +import sys +from pathlib import Path + +import numpy as np +import pytest +from astropy.io.fits import Header +from astropy.wcs import WCS + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT = REPO_ROOT / "workflow" / "scripts" / "exp_footprint.py" + +EXP = "2605805" +# MegaCam-ish: 2048 x 4612 pixels at 0.187"/pixel, i.e. ~0.106 x 0.240 deg. +NX, NY = 2048, 4612 +PIXSCALE = 0.187 / 3600.0 + + +def _load(): + """Import the script by path — ``workflow/scripts`` is not a package.""" + assert SCRIPT.exists(), f"{SCRIPT} not found; the rule calls it by path" + spec = importlib.util.spec_from_file_location("_exp_footprint", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +exp_footprint = _load() + + +def ccd_header(ra_centre, dec_centre): + """A single-CCD header + WCS, as split_exp stores them. + + The header carries plain ``NAXIS1/2`` and no ``ZIMAGE``, which is what + astropy hands back for a tile-compressed HDU once it is decompressed — the + case ``_image_shape`` takes its second branch for. + """ + w = WCS(naxis=2) + w.wcs.ctype = ["RA---TAN", "DEC--TAN"] + w.wcs.crpix = [NX / 2.0, NY / 2.0] + w.wcs.crval = [ra_centre, dec_centre] + w.wcs.cdelt = [-PIXSCALE, PIXSCALE] + header = w.to_header() + header["NAXIS"] = 2 + header["NAXIS1"] = NX + header["NAXIS2"] = NY + return {"WCS": w, "header": header.tostring()} + + +def headers_array(centres): + """``headers-.npy``'s content: object array, one entry per CCD.""" + arr = np.zeros(len(centres), dtype="O") + for i, (ra, dec) in enumerate(centres): + arr[i] = ccd_header(ra, dec) + return arr + + +# Six CCDs on a row, each 0.3 deg from the last so no two share a footprint, and +# CCD 3 sitting on the RA=0 seam. +CENTRES = [(10.0, 30.0), (10.3, 30.0), (10.6, 30.0), + (0.0, 30.0), (11.2, 30.0), (11.5, 30.0)] + + +@pytest.fixture +def store(tmp_path): + """A scratch exposure store with a headers npy, and a products root.""" + npy_dir = tmp_path / "exp" / EXP / exp_footprint.HEADERS_DIR + npy_dir.mkdir(parents=True) + np.save(npy_dir / f"headers-{EXP}.npy", headers_array(CENTRES)) + return tmp_path + + +def persist_manifest(path, ccds, patterns=("validation_psf-*.fits",)): + """An ``exp_persist`` manifest naming exactly ``ccds`` as PSF-bearing. + + Shaped as persist_exp.py writes it, including the decoy member: a keep list + of several patterns packs files this script must ignore, and reading a CCD + index out of one of them would be a real bug. + """ + files = [{"name": f"validation_psf-{EXP}-{c}.fits", + "pattern": "validation_psf-*.fits", "bytes": 1} for c in ccds] + files.append({"name": f"{EXP}-0.psf", "pattern": "*.psf", "bytes": 1}) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps( + {"stage": "exp_persist", "unit": EXP, "status": "complete", + "patterns": list(patterns), "n_files": len(files), "files": files}, + indent=2, sort_keys=True)) + return path + + +def run(store, persist, out, monkeypatch): + """Invoke the script's CLI, as the rule's shell does.""" + monkeypatch.setattr(sys, "argv", [ + "exp_footprint.py", + "--exp-dir", str(store / "exp" / EXP), + "--exp", EXP, + "--persist-manifest", str(persist), + "--manifest", str(out)]) + exp_footprint.main() + return json.loads(out.read_text()) + + +def test_ccd_index_alignment(store, monkeypatch): + """npy index ``i`` == ``-`` == ``validation_psf--.fits``. + + The three CCDs with a PSF are non-adjacent on purpose: an off-by-one or a + "renumber the survivors 0..n" bug passes a contiguous fixture and fails + this one. + """ + persist = persist_manifest(store / "prod" / "exp_persist.json", [0, 2, 5]) + record = run(store, persist, store / "prod" / "exp_footprint.json", + monkeypatch) + + assert record["n_ccd_headers"] == len(CENTRES) + assert record["n_valid_psf"] == 3 + assert [c["id"] for c in record["ccds"]] == [ + f"{EXP}-0", f"{EXP}-2", f"{EXP}-5"] + assert record["ccds_no_psf"] == [f"{EXP}-1", f"{EXP}-3", f"{EXP}-4"] + + # The corners on `-i` are the corners of ARRAY ELEMENT i, not of the + # i-th surviving CCD: each fixture CCD has its own centre, so this catches + # any permutation. Checked against the same two helpers the script imports, + # which is the point — the contract under test is the INDEXING, not the + # projection arithmetic (tests/module/test_coverage.py owns that). + arr = headers_array(CENTRES) + for entry in record["ccds"]: + i = int(entry["id"].rsplit("-", 1)[1]) + shape = exp_footprint._image_shape(Header.fromstring(arr[i]["header"])) + ra, dec = exp_footprint._ccd_corners(arr[i]["WCS"], shape) + assert entry["ra"] == pytest.approx(list(ra)) + assert entry["dec"] == pytest.approx(list(dec)) + # ... and that centre really is CCD i's, not its neighbour's. + assert np.mean(entry["dec"]) == pytest.approx(CENTRES[i][1], abs=1e-3) + + +def test_record_is_raw_sky_across_the_ra_seam(store, monkeypatch): + """CCD 3 straddles RA=0 and the record says so, unwrapped by nobody. + + The seam is handled once, in ``coverage_map_builder.unwrap_ra``, at the + moment a polygon is stamped. Unwrapping here as well would put negative RA + into a durable record that other consumers read, and double-unwrapping is + not idempotent. + """ + persist = persist_manifest(store / "prod" / "exp_persist.json", [3]) + record = run(store, persist, store / "prod" / "exp_footprint.json", + monkeypatch) + + ra = record["ccds"][0]["ra"] + assert record["ccds"][0]["id"] == f"{EXP}-3" + assert min(ra) >= 0.0 and max(ra) < 360.0, "raw sky, not a shifted branch" + assert max(ra) - min(ra) > 180.0, "the fixture must actually cross RA=0" + # The four corners land on both sides of the seam, ~0.05 deg out. + assert sorted(round(r) for r in ra) == [0, 0, 360, 360] + + +def test_byte_stable_rerun_keeps_the_mtime(store, monkeypatch): + """A rerun over an unchanged store must not move the manifest's mtime. + + mtime is a rerun trigger, and this manifest is an input of clean_exposure: + an unconditional rewrite would make every downstream reclamation look out + of date once per invocation. Same contract as persist_exp.py's. + """ + persist = persist_manifest(store / "prod" / "exp_persist.json", [0, 2, 5]) + out = store / "prod" / "exp_footprint.json" + + first = run(store, persist, out, monkeypatch) + raw = out.read_bytes() + before = out.stat().st_mtime_ns + + second = run(store, persist, out, monkeypatch) + assert second == first + assert out.read_bytes() == raw + assert out.stat().st_mtime_ns == before + # No stray tmp left behind on the persistent root. + assert not list(out.parent.glob("*.tmp")) + + +def test_keep_list_without_the_psf_pattern_is_fatal(store, monkeypatch): + """A manifest that packs no validation_psf files names no valid-PSF set. + + Writing an empty footprint there would be indistinguishable from an + exposure that genuinely lost every CCD, and the map would silently lose a + whole exposure's worth of sky. + """ + persist = persist_manifest(store / "prod" / "exp_persist.json", [], + patterns=("*.psf",)) + with pytest.raises(SystemExit) as exc: + run(store, persist, store / "prod" / "exp_footprint.json", monkeypatch) + assert "persist_exp" in str(exc.value) + + +def test_psf_for_a_ccd_the_split_never_wrote_is_fatal(store, monkeypatch): + """The one disagreement the index alignment cannot absorb, made loud.""" + persist = persist_manifest(store / "prod" / "exp_persist.json", [0, 99]) + with pytest.raises(SystemExit) as exc: + run(store, persist, store / "prod" / "exp_footprint.json", monkeypatch) + assert "99" in str(exc.value) + + +def test_missing_headers_array_is_fatal(store, monkeypatch): + """A purged or unbuilt split store fails here, never against VOS. + + This is the other half of the rule declaring only its DURABLE input + (exposure.smk): a persist manifest outliving its scratch store is a real + state, and it must cost one error line rather than an exposure rebuild. + """ + npy = (store / "exp" / EXP / exp_footprint.HEADERS_DIR + / f"headers-{EXP}.npy") + npy.unlink() + persist = persist_manifest(store / "prod" / "exp_persist.json", [0]) + with pytest.raises(SystemExit) as exc: + run(store, persist, store / "prod" / "exp_footprint.json", monkeypatch) + assert "no WCS array" in str(exc.value) diff --git a/workflow/scripts/exp_footprint.py b/workflow/scripts/exp_footprint.py index 305a72aaf..4ccec46cd 100644 --- a/workflow/scripts/exp_footprint.py +++ b/workflow/scripts/exp_footprint.py @@ -137,7 +137,12 @@ def footprint(headers, exp, with_psf): # from the stored header text (module docstring). shape = _image_shape(Header.fromstring(entry["header"])) ra, dec = _ccd_corners(entry["WCS"], shape) - ccds.append({"id": ccd_id, "ra": ra, "dec": dec}) + # float(), because _ccd_corners hands back numpy scalars and this record + # has to be byte-stable: a plain double's repr is, a numpy type's + # serialisation is json's business rather than ours. + ccds.append({"id": ccd_id, + "ra": [float(x) for x in ra], + "dec": [float(x) for x in dec]}) return ccds, no_psf From ac2af78298e81c0969a3e0a0e946cc5d57411b96 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 17:44:01 +0200 Subject: [PATCH 14/36] feat(workflow): build the campaign coverage mask as a workflow product MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New rules/coverage.smk with one campaign-level rule, `coverage_map`, gated by `coverage: {enabled: ...}` in config.yaml (off by default — the map is a campaign-END product). It stamps every exposure footprint into /coverage/coverage.hsp, a HealSparse map counting exposures with a valid PSF per sky pixel, and writes a manifest naming which exposures went in. CAMPAIGN-CUMULATIVE. Declared inputs are the in-scope, non-tombstoned footprints — ordering and rerun semantics without dragging out-of-scope tiles into the DAG — while the script globs EVERY record on the products root, reclaimed exposures included. Their records outlive their scratch stores and are still valid sky, so appending tiles grows the map instead of replacing it. The production nside pair 128/131072 is now config, not folklore: ~0.1"/pixel, chosen to match the UNIONS bit-mask resolution so coverage and mask align pixel-wise. It is NOT the CoverageMapBuilder default (32/2048), and a map built at the default would look entirely reasonable and would not align. Until now those two numbers lived only in scripts/sh/build_and_plot_coverage_maps.sh and docs/source/pipeline_canfar.md, both slated for deletion. Two parse-time guards, both failing before submission: `coverage:` on with no validation_psf pattern in `persist_exp:` (there is then no valid-PSF set to build from), and an nside that is not a power of two. coverage_map_builder gains the array entry point the workflow needs — build_map(ccd_ids, ra, dec, ...) plus load_corners() for the text path — so the CLI and the rule share one implementation of the RA-seam guard, the pole guard and the polygon accumulation. The exp_ra_dec.txt path is unchanged and still exercised by tests/module/test_coverage.py. Plots stay out of the DAG (plot_coverage_map by hand, windows under coverage.plot), by the argument that keeps run_report.py standalone. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- .../utilities/coverage_map_builder.py | 160 +++++++++++++----- tests/module/test_coverage_map.py | 142 ++++++++++++++++ workflow/README.md | 29 +++- workflow/Snakefile | 4 + workflow/config.yaml | 34 ++-- workflow/rules/coverage.smk | 97 +++++++++++ workflow/scripts/coverage_map.py | 133 +++++++++++++++ 7 files changed, 537 insertions(+), 62 deletions(-) create mode 100644 tests/module/test_coverage_map.py create mode 100644 workflow/rules/coverage.smk create mode 100644 workflow/scripts/coverage_map.py diff --git a/src/shapepipe/utilities/coverage_map_builder.py b/src/shapepipe/utilities/coverage_map_builder.py index c7144ef5c..361ace9e6 100644 --- a/src/shapepipe/utilities/coverage_map_builder.py +++ b/src/shapepipe/utilities/coverage_map_builder.py @@ -52,6 +52,115 @@ def unwrap_ra(ra): return ra +def load_corners(input_file): + """Read per-CCD corners from an ``exp_ra_dec.txt``. + + The nine-column text format the pre-Snakemake chain appended to: column 0 is + a ``-`` string ID, the remaining eight are the 4 RA then the + 4 Dec corners. The Snakemake workflow feeds :func:`build_map` arrays + directly instead (one JSON record per exposure, written by the + ``exp_footprint`` rule), so this loader exists only for the CLI. + + Parameters + ---------- + input_file : str + path to the per-CCD corner file + + Returns + ------- + tuple + ``(ccd_ids, ra, dec)``: a length-N string array and two ``(N, 4)`` + float arrays, in degrees + + """ + ccd_ids = np.atleast_1d(np.loadtxt(input_file, usecols=(0), dtype=str)) + corners = np.atleast_2d( + np.loadtxt(input_file, usecols=range(1, 9), dtype=float) + ) + return ccd_ids, corners[:, :4], corners[:, 4:] + + +def build_map( + ccd_ids, ra, dec, nside_coverage, nside, verbose=False +): + """Stamp one polygon per CCD footprint into a HealSparse nexp map. + + The array entry point, and the one both callers go through: the + ``build_coverage_map`` CLI (which reads its arrays out of a text file with + :func:`load_corners`) and the Snakemake ``coverage_map`` rule (which reads + them out of the per-exposure ``exp_footprint.json`` records). Keeping the + stamping in one place is what keeps the two ways of getting the same map + from drifting — the RA-seam and pole guards below are survey geometry, not + input-format handling. + + Since the CCDs of a single exposure do not overlap, accumulating value 1 per + CCD polygon makes the map count, per sky pixel, the number of exposures with + a valid PSF model covering it. + + Parameters + ---------- + ccd_ids : array_like + length-N CCD IDs, used only in the skipped-polygon warnings + ra : array_like + ``(N, 4)`` polygon RA corners, in degrees + dec : array_like + ``(N, 4)`` polygon Dec corners, in degrees + nside_coverage : int + HealSparse coverage nside + nside : int + HealSparse map nside + verbose : bool, optional + print progress + + Returns + ------- + healsparse.HealSparseMap + the nexp map + + """ + ra = np.atleast_2d(np.asarray(ra, dtype=float)) + dec = np.atleast_2d(np.asarray(dec, dtype=float)) + + if verbose: + print( + f"Creating HealSparse map (nside_coverage={nside_coverage}," + f" nside={nside})" + ) + m = hsp.HealSparseMap.make_empty(nside_coverage, nside, np.uint16) + + if verbose: + print("Adding polygons to map") + + n_added = 0 + n_skipped = 0 + for i in range(len(ccd_ids)): + dec_i = dec[i] + + # Pole guard: HealSparse's planar polygon fill degrades near the + # poles. CCD footprints never reach here, so warn and skip. + if np.any(np.abs(dec_i) >= _DEC_POLE_LIMIT): + print( + f"Warning: skipping CCD {ccd_ids[i]} with |dec| >= " + f"{_DEC_POLE_LIMIT} (too close to a pole)" + ) + n_skipped += 1 + continue + + # RA-wrap guard: put corners on a common branch across the seam. + ra_i = unwrap_ra(ra[i]) + + m += hsp.Polygon(ra=list(ra_i), dec=list(dec_i), value=1) + n_added += 1 + + if verbose and i % 1000 == 0: + print(f"{i:6d} / {len(ccd_ids):6d}") + + print(f"Added {n_added} polygons to map") + if n_skipped > 0: + print(f"Skipped {n_skipped} polygons near a pole") + + return m + class CoverageMapBuilder(object): """Coverage Map Builder Class. @@ -262,56 +371,13 @@ def run(self, args=None): if verbose: print(f"Reading per-CCD corners from: {input_file}") - # Load per-CCD corner data. Column 0 is a "-" string - # ID; the remaining 8 columns are the 4 RA then 4 Dec corners. - ccd_ids = np.atleast_1d(np.loadtxt(input_file, usecols=(0), dtype=str)) - corners = np.atleast_2d( - np.loadtxt(input_file, usecols=range(1, 9), dtype=float) - ) - ra_all = corners[:, :4] - dec_all = corners[:, 4:] + ccd_ids, ra_all, dec_all = load_corners(input_file) print(f"Loaded {len(ccd_ids)} CCD footprints") - if verbose: - print( - f"Creating HealSparse map (nside_coverage={nside_coverage}, nside={nside})" - ) - - # Create empty map - m = hsp.HealSparseMap.make_empty(nside_coverage, nside, np.uint16) - - # Add one polygon per CCD footprint - if verbose: - print("Adding polygons to map") - - n_added = 0 - n_skipped = 0 - for i in range(len(ccd_ids)): - dec = dec_all[i] - - # Pole guard: HealSparse's planar polygon fill degrades near the - # poles. CCD footprints never reach here, so warn and skip. - if np.any(np.abs(dec) >= _DEC_POLE_LIMIT): - print( - f"Warning: skipping CCD {ccd_ids[i]} with |dec| >= " - f"{_DEC_POLE_LIMIT} (too close to a pole)" - ) - n_skipped += 1 - continue - - # RA-wrap guard: put corners on a common branch across the seam. - ra = unwrap_ra(ra_all[i]) - - m += hsp.Polygon(ra=list(ra), dec=list(dec), value=1) - n_added += 1 - - if verbose and i % 1000 == 0: - print(f"{i:6d} / {len(ccd_ids):6d}") - - print(f"Added {n_added} polygons to map") - if n_skipped > 0: - print(f"Skipped {n_skipped} polygons near a pole") + m = build_map( + ccd_ids, ra_all, dec_all, nside_coverage, nside, verbose=verbose + ) # Apply median filter if requested if apply_median_filter: diff --git a/tests/module/test_coverage_map.py b/tests/module/test_coverage_map.py new file mode 100644 index 000000000..ea10890f9 --- /dev/null +++ b/tests/module/test_coverage_map.py @@ -0,0 +1,142 @@ +"""The workflow's coverage map: exposure footprint records -> a HealSparse nexp map. + +``tests/module/test_coverage.py`` pins the nexp contract through the +``exp_ra_dec.txt`` CLI path. This module pins the SAME contract through the path +the Snakemake workflow actually takes — one ``exp_footprint.json`` per exposure +on the products root, globbed and fed to ``build_map`` as arrays — because that +path has two properties the text one does not and neither is visible in the map: + + * it is CAMPAIGN-CUMULATIVE by construction. The script globs every record + under ``/exp/*/*/manifests/``, sharded, including exposures + whose scratch stores were reclaimed. A regression that fed it only the + declared rule inputs would build a map of one batch and look fine. + * the records are RAW SKY. Unwrapping across RA=0 happens once, inside + ``build_map``; a record written pre-unwrapped, or unwrapped twice, silently + moves a footprint by 360 degrees. + +Two exposures, one CCD each, offset so they overlap — the same fixture geometry +as the CLI test, so the two routes are comparable by eye. +""" + +import importlib.util +import json +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT = REPO_ROOT / "workflow" / "scripts" / "coverage_map.py" + + +def _load(): + """Import the script by path — ``workflow/scripts`` is not a package.""" + assert SCRIPT.exists(), f"{SCRIPT} not found; the rule calls it by path" + spec = importlib.util.spec_from_file_location("_coverage_map", SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +coverage_map = _load() + + +def write_footprint(products, exp, ccds): + """One exposure's record, at the sharded path exp_footprint writes to.""" + path = (products / "exp" / exp[:2] / exp / "manifests" + / "exp_footprint.json") + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps({ + "stage": "exp_footprint", "level": "exp", "unit": exp, + "status": "complete", + "n_ccd_headers": len(ccds), "n_valid_psf": len(ccds), + "ccds": [{"id": f"{exp}-{i}", "ra": ra, "dec": dec} + for i, (ra, dec) in enumerate(ccds)], + "ccds_no_psf": [], + }, indent=2, sort_keys=True)) + return path + + +def box(ra_min, ra_max, dec_min, dec_max): + """A rectangular CCD footprint, corners counterclockwise from bottom-left.""" + return ([ra_min, ra_max, ra_max, ra_min], + [dec_min, dec_min, dec_max, dec_max]) + + +def run(products, tmp_path, monkeypatch, nside="1024"): + out = tmp_path / "coverage" / "coverage.hsp" + manifest = tmp_path / "coverage" / "manifests" / "coverage_map.json" + monkeypatch.setattr(sys, "argv", [ + "coverage_map.py", + "--products-dir", str(products), + "--out", str(out), "--manifest", str(manifest), + "--nside-coverage", "32", "--nside", nside]) + coverage_map.main() + import healsparse as hsp + return hsp.HealSparseMap.read(str(out)), json.loads(manifest.read_text()) + + +def test_two_exposures_accumulate_to_nexp(tmp_path, monkeypatch): + """Overlapping CCDs from two exposures give 2 in the overlap, 1 outside. + + The exposures live in DIFFERENT shard directories, which is the campaign + layout: a glob that assumed one shard would find only one of them. + """ + products = tmp_path / "products" + write_footprint(products, "1000001", [box(9.8, 10.2, 19.8, 20.2)]) + write_footprint(products, "2000001", [box(10.0, 10.4, 19.8, 20.2)]) + + m, manifest = run(products, tmp_path, monkeypatch) + + values = m[m.valid_pixels] + assert values.max() == 2 + assert values.min() == 1 + assert (values == 2).sum() > 0 + assert (values == 1).sum() > 0 + + assert manifest["n_exposures"] == 2 + assert manifest["exposures"] == ["1000001", "2000001"] + assert manifest["n_ccds"] == 2 + assert manifest["nside_coverage"] == 32 + + +def test_every_record_on_the_root_is_used(tmp_path, monkeypatch): + """The map is campaign-cumulative: nothing selects a subset of the records. + + A third exposure appears on the products root with no involvement from any + caller — the state a reclaimed or out-of-scope exposure is in — and it must + still be in the map. + """ + products = tmp_path / "products" + write_footprint(products, "1000001", [box(9.8, 10.2, 19.8, 20.2)]) + write_footprint(products, "2000001", [box(10.0, 10.4, 19.8, 20.2)]) + write_footprint(products, "3000001", [box(40.0, 40.4, 19.8, 20.2)]) + + m, manifest = run(products, tmp_path, monkeypatch) + + assert manifest["exposures"] == ["1000001", "2000001", "3000001"] + # The far-away exposure is real sky in the map, not just a row in the record. + assert m.get_values_pos(40.2, 20.0, lonlat=True) == 1 + + +def test_seam_record_is_unwrapped_by_the_builder(tmp_path, monkeypatch): + """A raw-sky record across RA=0 lands where it belongs, not 360 deg away.""" + products = tmp_path / "products" + write_footprint(products, "1000001", + [([359.8, 0.2, 0.2, 359.8], [19.8, 19.8, 20.2, 20.2])]) + + m, _ = run(products, tmp_path, monkeypatch) + + assert m.get_values_pos(0.0, 20.0, lonlat=True) == 1 + assert m.get_values_pos(359.9, 20.0, lonlat=True) == 1 + # Nothing was stamped on the far side of the sky. + assert m.get_values_pos(180.0, 20.0, lonlat=True) == 0 + + +def test_no_records_is_a_loud_failure(tmp_path, monkeypatch): + """An empty products root must not write an empty map and exit green.""" + products = tmp_path / "products" + products.mkdir() + with pytest.raises(SystemExit) as exc: + run(products, tmp_path, monkeypatch) + assert "nothing to build a map from" in str(exc.value) diff --git a/workflow/README.md b/workflow/README.md index e533ab725..6299fc2d7 100644 --- a/workflow/README.md +++ b/workflow/README.md @@ -154,8 +154,9 @@ workflow/ bin/sp committed launcher (module load + /project venv + launch code snapshot + run/report/container/cancel) rules/ prepare.smk tile get_images/uncompress/find_exposures - exposure.smk per-exposure: get_images, star_cat, split, mask, psf, persist (no temp()) + exposure.smk per-exposure: get_images, star_cat, split, mask, psf, persist, footprint (no temp()) tile.smk per-tile: exp forest, merge_headers, mask, detect, vignets, ngmix, merge, make_cat + coverage.smk campaign-level: the HealSparse nexp mask from the exposure footprints scripts/ sp_rule.py the thin per-unit wrapper (isolation furniture, config copy, log-sync, count floor) build_index.py prepare-phase run_index.sqlite builder (plain script) @@ -164,6 +165,8 @@ workflow/ run_report.py standalone report (NOT a DAG node; run_report hooks call it) container.py image layers + the resolution order behind `sp container` (stdlib-only) persist_exp.py ONE exposure's keepable PSF products -> one tar on products_dir (the exp_persist rule) + exp_footprint.py ONE exposure's per-CCD sky corners, for the CCDs with a PSF (the exp_footprint rule) + coverage_map.py every exposure footprint on products_dir -> coverage.hsp (the coverage_map rule) clean_exposure.py ONE exposure's store + manifests + logs -> tombstone (the clean_exposure rule) profiles/nibi/config.yaml SLURM executor; apptainer SDM; per-user jobs cap; keep-going ``` @@ -247,6 +250,30 @@ profiles/nibi/config.yaml SLURM executor; apptainer SDM; per-user jobs cap; kee hours of PSF fitting per exposure. A pattern that matches nothing is a recorded warning (setools rejects sparse CCDs); matching nothing at all is a failure. A `localrule`, like `exp_star_cat` and for the same arithmetic. +- **Coverage is a workflow product, built from records the DAG already + writes.** `exp_footprint` writes one JSON per exposure to + `/exp///manifests/exp_footprint.json`, giving the + four sky corners of every CCD that got a PSF model. It reads the valid-PSF CCD + set off `exp_persist.json`'s tar members — exact, because `psfex_interp` + returns *without* writing `validation_psf-*.fits` on NOT_ENOUGH_STARS, + BAD_CHI2 or FILE_NOT_FOUND — and the WCS off `headers-.npy`, written by + `exp_split`. That makes `validation_psf-*` in `persist_exp:` a **precondition** + of the whole chain, not a preference. Like `exp_persist` it runs whatever + `clean:` and `coverage:` say, because its input is on /scratch and the purge + takes it; `clean_exposure` takes its manifest as an input. Set + `coverage: {enabled: true}` and one further job, `coverage_map`, stamps every + footprint into `/coverage/coverage.hsp` — a HealSparse map + counting, per sky pixel, the exposures with a valid PSF there. That job is + **campaign-cumulative**: its declared inputs are the in-scope footprints, but + the script reads *every* record on the products root, reclaimed exposures + included, so appending tiles grows the map instead of replacing it. `nside` + defaults to the production 128/131072 pair, ~0.1"/pixel, chosen to align + pixel-wise with the UNIONS bit masks — not the `CoverageMapBuilder` class + defaults (32/2048), which would produce a plausible-looking map that does not + align. Plotting stays out of the DAG: run `plot_coverage_map -i + /coverage/coverage.hsp ...` by hand, with the sky windows under + `coverage.plot` in `config.yaml`. sp_validation consumes the map in + `notebooks/demo_apply_hsp_masks.py`. - **A dead tile can be told to stop pinning exposures.** An exposure is cleanable only once every consuming tile has its vignets, so one permanently-failed tile holds its ~80 exposures for the life of the diff --git a/workflow/Snakefile b/workflow/Snakefile index 0ddaacec4..1757ceaf0 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -677,6 +677,7 @@ def sp_shell(stage, config_name, *, check_args="", post=""): include: "rules/prepare.smk" include: "rules/exposure.smk" include: "rules/tile.smk" +include: "rules/coverage.smk" # --- top-level targets ------------------------------------------------------ # The aggregation targets, clean_exposure, clean_tile, star_catalogue, @@ -691,6 +692,8 @@ include: "rules/tile.smk" # ~20k of them at DR6 scale, each far shorter than the scheduling latency that # would submit it (exposure.smk argues the placement in full). exp_footprint sits # beside it and is smaller still: one pickle load and ~160 pixel_to_world calls. +# coverage_map is deliberately NOT one — it is a single submitted job that stamps +# ~1M polygons at nside=131072 (coverage.smk). # # star_catalogue and exp_star_cat are MID-CHAIN localrules, so they must stay out # of any future `group:` label: a local job cannot be fused into a submitted group. @@ -704,6 +707,7 @@ rule all: [final_cat(t) for t in TILES_READY], persist_targets(), footprint_targets(), + coverage_targets(), clean_targets(), clean_tile_targets(), diff --git a/workflow/config.yaml b/workflow/config.yaml index b205543ef..2c5234a78 100644 --- a/workflow/config.yaml +++ b/workflow/config.yaml @@ -223,9 +223,14 @@ clean_ignore_tiles: [] # per-tile, in-job, from the tile's own sexcat). ngmix_chunks: 8 -# COVERAGE MASK (not wired yet — the rules that read this block land in a later -# commit; the keys are parked here so the two numbers below survive the deletion -# of the only two files that ever carried them). +# COVERAGE MASK. `enabled: true` adds ONE campaign-level job, `coverage_map` +# (workflow/rules/coverage.smk), which builds a HealSparse nexp map from every +# exposure footprint on the products root. OFF by default because the map is a +# campaign-END product: a half-finished campaign's map is a picture of how far it +# has got, not of the survey. Turning it on costs one job and no rebuild — the +# per-exposure records it reads are written all along, by `exp_footprint`, +# whether or not this block is on (that rule is gated on the keep list below, +# not on this flag: its input is on /scratch and the purge takes it). # # The map counts, per sky pixel, the number of exposures with a VALID PSF MODEL # — only CCDs whose PSF model was actually written are stamped. In the v1.x @@ -249,17 +254,18 @@ ngmix_chunks: 8 # 1-5 exposure range; plotting stays OUT of the DAG (a human act on a durable # product), so these are defaults for `plot_coverage_map`, not rule params. # -# coverage: -# enabled: false -# nside_coverage: 128 -# nside: 131072 -# plot: -# colorbar: true -# n_exp_min: 1 -# n_exp_max: 5 -# regions: -# SGC: {ra_min: -20, ra_max: 45, dec_min: 18, dec_max: 40} -# NGC: {ra_min: 110, ra_max: 270, dec_min: 28, dec_max: 90} +coverage: + enabled: false + nside_coverage: 128 + nside: 131072 + # Read by `plot_coverage_map`, not by any rule (plotting stays out of the DAG). + plot: + colorbar: true + n_exp_min: 1 + n_exp_max: 5 + regions: + SGC: {ra_min: -20, ra_max: 45, dec_min: 18, dec_max: 40} + NGC: {ra_min: 110, ra_max: 270, dec_min: 28, dec_max: 90} # The container's installed shapepipe is overridden by the prod worktree via # --env PYTHONPATH in profiles/nibi (settled call 3); no config knob here. diff --git a/workflow/rules/coverage.smk b/workflow/rules/coverage.smk new file mode 100644 index 000000000..48e908562 --- /dev/null +++ b/workflow/rules/coverage.smk @@ -0,0 +1,97 @@ +"""Coverage — the campaign's HealSparse nexp mask, from the exposure footprints. + + exp_footprint (per exposure, exposure.smk) -> coverage_map (per campaign) + +One rule, and it is the only campaign-level product besides the report. The +precedent it follows is ``star_catalogue`` (exposure.smk): a rule keyed by the +campaign rather than by a unit, whose inputs are the units' own records. + +CAMPAIGN-CUMULATIVE. The declared inputs are the IN-SCOPE, non-tombstoned +footprint manifests — ordering and rerun semantics for free, without dragging +out-of-scope tiles into the DAG through a new target. The SCRIPT then reads every +footprint record on the persistent root, tombstoned exposures included: their +records outlive their scratch stores and are still valid sky. So appending tiles +grows the map instead of replacing it, which is what a survey coverage mask +should do, and rebuilding is one job rather than a campaign. + +NOT A LOCALRULE, unlike every other rule that writes to the persistent root: at +DR6 scale this stamps ~1M polygons at nside=131072 in a Python loop +(coverage_map_builder.build_map). The resources below are a first sizing from +that count and not a measurement — the polygon loop is unmeasured above a few +thousand CCDs. + +PLOTS STAY OUT OF THE DAG. `plot_coverage_map -i /coverage/ +coverage.hsp ...`, by hand, with the windows in config.yaml's `coverage.plot` +block — the same argument that keeps run_report.py a standalone script: it is a +human act on a durable product. +""" + +# `coverage:` is OFF by default: the map is a campaign-end product, and a +# half-finished campaign's map is a picture of how far it has got rather than of +# the survey. flag() because `--config` delivers booleans as strings. +COVERAGE = config.get("coverage") or {} +COVERAGE_ENABLED = flag(COVERAGE.get("enabled", False)) +NSIDE_COVERAGE = int(COVERAGE.get("nside_coverage", 128)) +NSIDE = int(COVERAGE.get("nside", 131072)) + +COVERAGE_DIR = f"{PRODUCTS_DIR}/coverage" +COVERAGE_HSP = f"{COVERAGE_DIR}/coverage.hsp" +COVERAGE_MANIFEST = f"{COVERAGE_DIR}/manifests/coverage_map.json" +COVERAGE_HASH = script_hash("coverage_map.py") + +# PARSE-TIME GUARDS, and both of them fail before submission rather than after. +# +# The first is the precondition the whole chain rests on: exp_footprint reads its +# valid-PSF CCD set off exp_persist.json's members, so with `validation_psf-*` +# absent from `persist_exp:` no footprint job exists, no record exists, and +# `coverage:` on would otherwise mean a green run producing nothing — or, worse, +# a map built from whatever stale records happened to be on the products root. +if COVERAGE_ENABLED and not PERSIST_HAS_PSF: + raise WorkflowError( + f"coverage: is enabled but persist_exp: {PERSIST_EXP} packs no " + f"{PSF_PRODUCT_PROBE}-like files. The coverage map is built from the " + f"valid-PSF CCD set, which is read off exp_persist's tar members — " + f"without that pattern there is no such set. Add it, or turn coverage " + f"off.") + +# The second: healsparse requires powers of two, and it says so only once a job +# has reached a node. +for _key, _n in (("nside_coverage", NSIDE_COVERAGE), ("nside", NSIDE)): + if COVERAGE_ENABLED and (_n <= 0 or _n & (_n - 1)): + raise WorkflowError(f"coverage.{_key} must be a power of 2, got {_n}") + + +def coverage_targets(): + """The campaign map, when `coverage:` asks for one. + + HEAD PROCESS ONLY, for the reason clean_targets() gives: this feeds `rule + all` at module level, so it is evaluated on every per-job re-parse too, none + of which can schedule `all`. + """ + if not COVERAGE_ENABLED or not workflow.is_main_process: + return [] + return [COVERAGE_HSP, COVERAGE_MANIFEST] + + +rule coverage_map: + input: + footprint_targets() + output: + hsp = COVERAGE_HSP, + manifest = COVERAGE_MANIFEST + # No `log:`: this is one job with one verdict, and its stderr is the job's. + params: + products = PRODUCTS_DIR, + nside_coverage = NSIDE_COVERAGE, + nside = NSIDE, + script_hash = COVERAGE_HASH + threads: 1 + resources: + mem_mb = 32000, + runtime = 240 + shell: + "set -euo pipefail\n" + f"python {SCRIPTS}/coverage_map.py" + " --products-dir '{params.products}'" + " --out {output.hsp} --manifest {output.manifest}" + " --nside-coverage {params.nside_coverage} --nside {params.nside}" diff --git a/workflow/scripts/coverage_map.py b/workflow/scripts/coverage_map.py new file mode 100644 index 000000000..0086a73b0 --- /dev/null +++ b/workflow/scripts/coverage_map.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Build the campaign's HealSparse coverage (nexp) map from the exposure footprints. + +Run as the shell of the in-DAG ``coverage_map`` rule, never by hand. + +WHAT THE MAP IS. Per sky pixel, the number of exposures with a VALID PSF MODEL +covering it. The CCDs of one exposure do not overlap, so stamping value 1 per CCD +polygon and accumulating counts exposures. sp_validation applies it as a +structural mask (``notebooks/demo_apply_hsp_masks.py``). + +CAMPAIGN-CUMULATIVE, AND THAT IS THE POINT. This script GLOBS every +``/exp/*/*/manifests/exp_footprint.json`` — not just the ones the +rule declared as inputs, and INCLUDING exposures whose scratch stores have been +reclaimed. The declared inputs are the in-scope, non-tombstoned footprints, which +is what buys ordering and rerun semantics without dragging out-of-scope tiles +into the DAG; the records themselves live on the persistent root and stay valid +sky forever. So appending tiles GROWS the map rather than replacing it, which is +what a survey coverage mask should do. The rule's comment says the same thing +where a reader of the DAG will meet it. + +THE STAMPING IS NOT HERE. It is ``shapepipe.utilities.coverage_map_builder. +build_map``, shared with the ``build_coverage_map`` CLI, so the RA-seam guard, +the pole guard and the polygon accumulation have exactly one implementation. This +script is the JSON-to-arrays half, and the records are raw sky: unwrapping across +RA=0 happens once, inside build_map, at the moment a polygon is stamped. + +NSIDE IS NOT DEFAULTED HERE. Both values are required arguments, carried from +`coverage:` in config.yaml, because the production pair (128 / 131072) is NOT the +CoverageMapBuilder default (32 / 2048) and the difference is invisible in the +output: nside=131072 is ~0.1"/pixel, chosen to match the UNIONS bit-mask +resolution so coverage and mask align pixel-wise. A map built at the class +default would look entirely reasonable and would not align, and the consumer +would not notice. +""" + +import argparse +import filecmp +import json +import sys +from pathlib import Path + +import numpy as np + +from shapepipe.utilities.coverage_map_builder import build_map + +# Where exp_footprint writes, relative to the products root. The shard and the +# exposure id are both globbed: this map is the whole campaign's, so it is +# deliberately not built from a list of units. +FOOTPRINT_GLOB = "exp/*/*/manifests/exp_footprint.json" + + +def read_footprints(products_dir): + """Every exposure footprint on the persistent root, as flat arrays. + + Returns ``(ccd_ids, ra, dec, units)``: length-M id and ``(M, 4)`` corner + arrays over all CCDs of all exposures, plus the sorted exposure ids that + contributed. Records are read in sorted path order so the polygon order — + and hence the map — does not depend on readdir order. + """ + paths = sorted(Path(products_dir).glob(FOOTPRINT_GLOB)) + if not paths: + sys.exit(f"coverage_map: no {FOOTPRINT_GLOB} under {products_dir}; " + f"there is nothing to build a map from") + + ccd_ids, ra, dec, units = [], [], [], [] + for path in paths: + body = json.loads(path.read_text()) + units.append(body["unit"]) + for ccd in body["ccds"]: + ccd_ids.append(ccd["id"]) + ra.append(ccd["ra"]) + dec.append(ccd["dec"]) + return (np.array(ccd_ids, dtype=str), + np.array(ra, dtype=float).reshape(-1, 4), + np.array(dec, dtype=float).reshape(-1, 4), + sorted(units)) + + +def write_stable(path, body): + """Write JSON tmp-then-``cmp``-then-``mv``; an unchanged body keeps its mtime.""" + path.parent.mkdir(parents=True, exist_ok=True) + tmp = path.with_name(path.name + ".tmp") + try: + tmp.write_text(json.dumps(body, indent=2, sort_keys=True) + "\n") + if path.exists() and filecmp.cmp(tmp, path, shallow=False): + tmp.unlink() + else: + tmp.replace(path) + finally: + tmp.unlink(missing_ok=True) + + +def main() -> None: + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--products-dir", required=True, type=Path, + help="the persistent root; every exposure footprint under " + "it goes into the map") + p.add_argument("--out", required=True, type=Path, + help="the HealSparse map, /coverage/coverage.hsp") + p.add_argument("--manifest", required=True, type=Path) + p.add_argument("--nside-coverage", required=True, type=int) + p.add_argument("--nside", required=True, type=int) + p.add_argument("--verbose", action="store_true") + args = p.parse_args() + + ccd_ids, ra, dec, units = read_footprints(args.products_dir) + print(f"[coverage_map] {len(units)} exposure(s), {len(ccd_ids)} CCD " + f"footprint(s) from {args.products_dir}") + + hsp_map = build_map(ccd_ids, ra, dec, args.nside_coverage, args.nside, + verbose=args.verbose) + + args.out.parent.mkdir(parents=True, exist_ok=True) + hsp_map.write(str(args.out), clobber=True) + + # The manifest is written AFTER the map, and it is the rule's record of + # which exposures the map contains — the question a mask's consumer asks + # months later, and one nothing else on disk can answer once the campaign + # has grown past it. + write_stable(args.manifest, { + "stage": "coverage_map", "level": "campaign", "status": "complete", + "map": str(args.out), + "nside_coverage": args.nside_coverage, + "nside": args.nside, + "n_exposures": len(units), + "n_ccds": len(ccd_ids), + "exposures": units, + }) + print(f"[coverage_map] -> {args.out}") + + +if __name__ == "__main__": + main() From 30c406fd7d07801bf83c32a1b5a4a9658543e96e Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 17:45:22 +0200 Subject: [PATCH 15/36] style: wrap the new coverage docstrings to the 79-col line length Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- .../utilities/coverage_map_builder.py | 24 +++++++++---------- tests/unit/test_exp_footprint.py | 1 - workflow/scripts/exp_footprint.py | 5 +++- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/shapepipe/utilities/coverage_map_builder.py b/src/shapepipe/utilities/coverage_map_builder.py index 361ace9e6..ffa2471fe 100644 --- a/src/shapepipe/utilities/coverage_map_builder.py +++ b/src/shapepipe/utilities/coverage_map_builder.py @@ -55,10 +55,10 @@ def unwrap_ra(ra): def load_corners(input_file): """Read per-CCD corners from an ``exp_ra_dec.txt``. - The nine-column text format the pre-Snakemake chain appended to: column 0 is - a ``-`` string ID, the remaining eight are the 4 RA then the - 4 Dec corners. The Snakemake workflow feeds :func:`build_map` arrays - directly instead (one JSON record per exposure, written by the + The nine-column text format the pre-Snakemake chain appended to: column + 0 is a ``-`` string ID, the remaining eight are the 4 RA + then the 4 Dec corners. The Snakemake workflow feeds :func:`build_map` + arrays directly instead (one JSON record per exposure, written by the ``exp_footprint`` rule), so this loader exists only for the CLI. Parameters @@ -86,16 +86,16 @@ def build_map( """Stamp one polygon per CCD footprint into a HealSparse nexp map. The array entry point, and the one both callers go through: the - ``build_coverage_map`` CLI (which reads its arrays out of a text file with - :func:`load_corners`) and the Snakemake ``coverage_map`` rule (which reads - them out of the per-exposure ``exp_footprint.json`` records). Keeping the - stamping in one place is what keeps the two ways of getting the same map - from drifting — the RA-seam and pole guards below are survey geometry, not + ``build_coverage_map`` CLI (which reads its arrays out of a text file + with :func:`load_corners`) and the Snakemake ``coverage_map`` rule (which + reads them out of the per-exposure ``exp_footprint.json`` records). The + stamping stays in one place so the two ways of getting the same map cannot + drift — the RA-seam and pole guards below are survey geometry, not input-format handling. - Since the CCDs of a single exposure do not overlap, accumulating value 1 per - CCD polygon makes the map count, per sky pixel, the number of exposures with - a valid PSF model covering it. + Since the CCDs of a single exposure do not overlap, accumulating value 1 + per CCD polygon makes the map count, per sky pixel, the number of + exposures with a valid PSF model covering it. Parameters ---------- diff --git a/tests/unit/test_exp_footprint.py b/tests/unit/test_exp_footprint.py index ddd244a6a..44da97a00 100644 --- a/tests/unit/test_exp_footprint.py +++ b/tests/unit/test_exp_footprint.py @@ -35,7 +35,6 @@ from astropy.io.fits import Header from astropy.wcs import WCS - REPO_ROOT = Path(__file__).resolve().parents[2] SCRIPT = REPO_ROOT / "workflow" / "scripts" / "exp_footprint.py" diff --git a/workflow/scripts/exp_footprint.py b/workflow/scripts/exp_footprint.py index 4ccec46cd..54aa90198 100644 --- a/workflow/scripts/exp_footprint.py +++ b/workflow/scripts/exp_footprint.py @@ -69,7 +69,10 @@ import numpy as np from astropy.io.fits import Header -from shapepipe.utilities.field_corners_extractor import _ccd_corners, _image_shape +from shapepipe.utilities.field_corners_extractor import ( + _ccd_corners, + _image_shape, +) # The split stage's run dir (RUN_NAME in config_exp_Sp.ini) and its module # output dir. Hardcoded rather than passed, for persist_exp.py's reason: this From c635f80904601099353935c46b45e2793576ea42 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:06:23 +0200 Subject: [PATCH 16/36] config: stop describing the ME_*_EXP_ keys by the keys they replaced The runner strips in this branch deleted ME_DOT_PSF_DIR and ME_IMAGE_DIR outright, so a comment saying ME_DOT_PSF_EXP_DIR "replaces ME_DOT_PSF_DIR" now points a reader at a key that raises nothing and does nothing -- the migration note outlived the migration. Say what the key is instead. Comment-only, same four .ini as the previous commit; .ini content is not hashed into any params, so this is not a rerun trigger. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- example/cfis/config_tile_PiViVi_canfar_sx.ini | 10 +++++----- example/cfis/config_tile_PiViVi_canfar_uc.ini | 10 +++++----- .../cfis_image_sims/config_tile_PiViVi_canfar_sx.ini | 4 ++-- workflow/config/cfis/config_tile_PiViVi.ini | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/example/cfis/config_tile_PiViVi_canfar_sx.ini b/example/cfis/config_tile_PiViVi_canfar_sx.ini index 8266ad03e..134ebf0b9 100644 --- a/example/cfis/config_tile_PiViVi_canfar_sx.ini +++ b/example/cfis/config_tile_PiViVi_canfar_sx.ini @@ -86,9 +86,9 @@ CHI2_THRESH = 2 # Multi-epoch mode parameters -# Root directory of per-exposure work directories; replaces ME_DOT_PSF_DIR -# for the per-exposure pipeline. psfex_runner/output/ dirs are discovered -# by scanning $SP_EXP for the exposures listed in the exp_numbers input file. +# Root directory of per-exposure work directories. psfex_runner/output/ +# dirs are discovered by scanning $SP_EXP for the exposures listed in the +# exp_numbers input file. ME_DOT_PSF_EXP_DIR = $SP_EXP # Input psf file pattern @@ -161,8 +161,8 @@ STAMP_SIZE = 51 PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure -# run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# run outputs. Output dirs are discovered by scanning $SP_EXP for the +# exposures listed in the exp_numbers input file. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner, sextractor_runner, sextractor_runner ME_IMAGE_PATTERN = flag, image, weight, background, background_rms diff --git a/example/cfis/config_tile_PiViVi_canfar_uc.ini b/example/cfis/config_tile_PiViVi_canfar_uc.ini index 63cd7beee..cc05969a1 100644 --- a/example/cfis/config_tile_PiViVi_canfar_uc.ini +++ b/example/cfis/config_tile_PiViVi_canfar_uc.ini @@ -84,9 +84,9 @@ CHI2_THRESH = 2 # Multi-epoch mode parameters -# Root directory of per-exposure work directories; replaces ME_DOT_PSF_DIR -# for the per-exposure pipeline. psfex_runner/output/ dirs are discovered -# by scanning $SP_EXP for the exposures listed in the exp_numbers input file. +# Root directory of per-exposure work directories. psfex_runner/output/ +# dirs are discovered by scanning $SP_EXP for the exposures listed in the +# exp_numbers input file. ME_DOT_PSF_EXP_DIR = $SP_EXP # Input psf file pattern @@ -159,8 +159,8 @@ STAMP_SIZE = 51 PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure -# run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# run outputs. Output dirs are discovered by scanning $SP_EXP for the +# exposures listed in the exp_numbers input file. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner, sextractor_runner, sextractor_runner ME_IMAGE_PATTERN = flag, image, weight, background, background_rms diff --git a/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini b/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini index 592778bef..164f6e69f 100644 --- a/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini +++ b/example/cfis_image_sims/config_tile_PiViVi_canfar_sx.ini @@ -120,8 +120,8 @@ STAMP_SIZE = 51 PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure -# run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# run outputs. Output dirs are discovered by scanning $SP_EXP for the +# exposures listed in the exp_numbers input file. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner ME_IMAGE_PATTERN = flag, image, weight diff --git a/workflow/config/cfis/config_tile_PiViVi.ini b/workflow/config/cfis/config_tile_PiViVi.ini index 6e9390734..c0793eed0 100644 --- a/workflow/config/cfis/config_tile_PiViVi.ini +++ b/workflow/config/cfis/config_tile_PiViVi.ini @@ -106,9 +106,9 @@ CHI2_THRESH = 2 # Multi-epoch mode parameters -# Root directory of per-exposure work directories; replaces ME_DOT_PSF_DIR -# for the per-exposure pipeline. psfex_runner/output/ dirs are discovered -# by scanning $SP_EXP for the exposures listed in the exp_numbers input file. +# Root directory of per-exposure work directories. psfex_runner/output/ +# dirs are discovered by scanning $SP_EXP for the exposures listed in the +# exp_numbers input file. ME_DOT_PSF_EXP_DIR = $SP_EXP # Input psf file pattern @@ -181,8 +181,8 @@ STAMP_SIZE = 51 PREFIX = # Additional parameters for path and file pattern corresponding to single-exposure -# run outputs. ME_IMAGE_EXP_DIR/ME_IMAGE_EXP_RUNNERS replace ME_IMAGE_DIR for -# the per-exposure pipeline; output dirs are discovered by scanning $SP_EXP. +# run outputs. Output dirs are discovered by scanning $SP_EXP for the +# exposures listed in the exp_numbers input file. ME_IMAGE_EXP_DIR = $SP_EXP ME_IMAGE_EXP_RUNNERS = split_exp_runner, split_exp_runner, split_exp_runner, sextractor_runner, sextractor_runner ME_IMAGE_PATTERN = flag, image, weight, background, background_rms From 8d5956f4ab2292ab91bc94b238b84b392d2cb42c Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:13:21 +0200 Subject: [PATCH 17/36] fix(coverage): a root of records that name no CCD is not an empty survey read_footprints already refuses to build from a products root with no footprint records. The other empty map is quieter and worse: records present, every one of them naming zero CCDs. That is a broken PSF stage, not a survey with no coverage, and the .hsp it writes is valid, plausible-looking and masks everything downstream. Fail there too, naming how many records were read. Also names exp_footprint beside exp_persist in run_report's EXP_STAGES comment: it is now the second exposure manifest living on products_dir rather than in the tree this report scans, and the comment is where the next person adding a stage will look. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- tests/module/test_coverage_map.py | 16 ++++++++++++++++ workflow/scripts/coverage_map.py | 11 +++++++++++ workflow/scripts/run_report.py | 12 ++++++------ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/tests/module/test_coverage_map.py b/tests/module/test_coverage_map.py index ea10890f9..7ba895066 100644 --- a/tests/module/test_coverage_map.py +++ b/tests/module/test_coverage_map.py @@ -140,3 +140,19 @@ def test_no_records_is_a_loud_failure(tmp_path, monkeypatch): with pytest.raises(SystemExit) as exc: run(products, tmp_path, monkeypatch) assert "nothing to build a map from" in str(exc.value) + + +def test_records_naming_no_ccd_is_a_loud_failure(tmp_path, monkeypatch): + """Records that name no CCD are the quieter empty map, and equally fatal. + + Every exposure on the root having lost every CCD is a broken PSF stage, not + a survey with no coverage — but the .hsp it would write is valid and + plausible, and its consumer would mask everything. + """ + products = tmp_path / "products" + write_footprint(products, "1000001", []) + write_footprint(products, "2000001", []) + + with pytest.raises(SystemExit) as exc: + run(products, tmp_path, monkeypatch) + assert "not one names a CCD with a PSF model" in str(exc.value) diff --git a/workflow/scripts/coverage_map.py b/workflow/scripts/coverage_map.py index 0086a73b0..0a2e968bd 100644 --- a/workflow/scripts/coverage_map.py +++ b/workflow/scripts/coverage_map.py @@ -70,6 +70,17 @@ def read_footprints(products_dir): ccd_ids.append(ccd["id"]) ra.append(ccd["ra"]) dec.append(ccd["dec"]) + + # Records but no CCDs is the other empty map, and it is the quieter one: it + # means every exposure on the root lost every CCD, which is a broken PSF + # stage rather than a survey with no coverage. Written out it would be a + # valid, plausible-looking .hsp full of nothing, and its consumer masks + # everything. + if not ccd_ids: + sys.exit(f"coverage_map: {len(paths)} footprint record(s) under " + f"{products_dir}, and not one names a CCD with a PSF model; " + f"there is nothing to stamp") + return (np.array(ccd_ids, dtype=str), np.array(ra, dtype=float).reshape(-1, 4), np.array(dec, dtype=float).reshape(-1, 4), diff --git a/workflow/scripts/run_report.py b/workflow/scripts/run_report.py index 0a48605b8..8bf4908b5 100644 --- a/workflow/scripts/run_report.py +++ b/workflow/scripts/run_report.py @@ -61,12 +61,12 @@ "tile_ngmix", "tile_merge_cats", "tile_make_cat"] EXP_STAGES = ["exp_get_images", "exp_star_cat", "exp_split", "exp_mask", "exp_psf"] -# exp_persist is DELIBERATELY NOT in that list. This report disk-scans the -# scratch run_dir, and exp_persist's manifest is the one exposure manifest that -# lives on products_dir instead — that placement is what makes it survive -# clean_exposure. Listed here it would read as "not run" for every exposure in -# the campaign. Reporting on the persisted products means scanning the second -# root, which is a report this one does not yet do. +# exp_persist and exp_footprint are DELIBERATELY NOT in that list. This report +# disk-scans the scratch run_dir, and those two are the exposure manifests that +# live on products_dir instead — that placement is what makes them survive +# clean_exposure. Listed here they would read as "not run" for every exposure +# in the campaign. Reporting on the persisted products means scanning the +# second root, which is a report this one does not yet do. # The manifests clean_tile leaves on disk (workflow/scripts/clean_tile.py names # the mechanism that owns each). Their presence is therefore NOT evidence that a From 9f293ae144092f1a28e78208889c43542d883230 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:18:49 +0200 Subject: [PATCH 18/36] refactor(coverage): retire the version-keyed CCD/PSF and header chain The v1.x coverage mask was assembled by scraping a finished campaign: read a catalogue version's patch dirs for the CCDs that got a PSF model, download ~25k FITS headers from VOSpace because the run kept no local WCS, and drive the pair from a shell loop over v1.3..v1.6. None of those premises hold. The valid-PSF CCD set is now named exactly by exp_persist.json's tar members, the WCS is already on scratch as headers-.npy, and the version is the git tag rather than a key the code branches on. The exp_footprint rule reads both and the coverage_map rule stamps the map, so this chain has neither a producer nor a consumer left. Deleted: ccd_psf_handler.py (the version->n_patch table, -V, the P1..P9 loop and the second v1.3 method), header_downloader.py, get_ccds_run.py, its uninstalled duplicate scripts/python/get_ccds_with_psf.py, and build_and_plot_coverage_maps.sh, whose two load-bearing numbers (nside 128/131072, matching the UNIONS bit masks) and SGC/NGC plot windows now live in workflow/config.yaml's coverage: block. The get_ccds_with_psf and download_headers entry points go in the same commit as their modules: test_entrypoints.py parametrizes over that table at import. test_coverage.py loses the tests for the deleted code, including the summary.get_all_shdus assertion that pinned the - ID format -- tests/unit/test_exp_footprint.py now pins that same contract where it is actually relied on. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- pyproject.toml | 2 - scripts/python/get_ccds_with_psf.py | 59 --- scripts/sh/build_and_plot_coverage_maps.sh | 95 ----- src/shapepipe/coverage_run.py | 21 - src/shapepipe/get_ccds_run.py | 44 --- src/shapepipe/utilities/ccd_psf_handler.py | 387 ------------------- src/shapepipe/utilities/header_downloader.py | 291 -------------- tests/module/test_coverage.py | 151 +------- 8 files changed, 7 insertions(+), 1043 deletions(-) delete mode 100755 scripts/python/get_ccds_with_psf.py delete mode 100755 scripts/sh/build_and_plot_coverage_maps.sh delete mode 100644 src/shapepipe/get_ccds_run.py delete mode 100644 src/shapepipe/utilities/ccd_psf_handler.py delete mode 100644 src/shapepipe/utilities/header_downloader.py diff --git a/pyproject.toml b/pyproject.toml index 5333aa91c..1010b4b47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,8 +88,6 @@ summary_run = "shapepipe.summary_run:main" canfar_submit_job = "shapepipe.canfar_run:run_job" canfar_monitor = "shapepipe.canfar_run:run_log" canfar_monitor_log = "shapepipe.canfar_run:run_monitor_log" -get_ccds_with_psf = "shapepipe.get_ccds_run:run_ccd_psf_handler" -download_headers = "shapepipe.coverage_run:run_download_headers" extract_field_corners = "shapepipe.coverage_run:run_extract_corners" build_coverage_map = "shapepipe.coverage_run:run_build_coverage" plot_coverage_map = "shapepipe.coverage_run:run_plot_coverage" diff --git a/scripts/python/get_ccds_with_psf.py b/scripts/python/get_ccds_with_psf.py deleted file mode 100755 index e284e2640..000000000 --- a/scripts/python/get_ccds_with_psf.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 - -"""GET_CCDS_WITH_PSF - -Obtain list of CCDs (single-exposure single-HDU files) for which valid PSF information -is available. This can serve to create a footprint coverage mask. - -Author: Martin Kilbinger - -""" - -import sys - -from shapepipe.utilities.ccd_psf_handler import CcdPsfHandler - - -def run_ccd_psf_handler(args=None): - """Run CCD PSF Handler. - - Create instance and run the CCD PSF handler. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code - - """ - # Create instance - obj = CcdPsfHandler() - - return obj.run(args=args) - - -def main(argv=None): - """Main. - - Main program. - - Parameters - ---------- - argv : list, optional - command line arguments - - Returns - ------- - int - exit code - - """ - return run_ccd_psf_handler(args=argv) - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/scripts/sh/build_and_plot_coverage_maps.sh b/scripts/sh/build_and_plot_coverage_maps.sh deleted file mode 100755 index b7ea9c8b2..000000000 --- a/scripts/sh/build_and_plot_coverage_maps.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# Build and plot per-CCD coverage (nexp) maps for a range of catalogue -# versions. Each map counts, per sky pixel, the number of exposures with a -# valid PSF model. The per-version flow is: -# 0. get_ccds_with_psf -> ccds_with_psf_.txt (valid CCD IDs) -# 1. download_headers -> per-exposure header text files -# 2. extract_field_corners-> exp_ra_dec_.txt (per-CCD corners) -# 3. build_coverage_map -> coverage_.x.hsp -# 4. plot_coverage_map -> SGC / NGC plots -# Steps 0-2 are expected to have been run already on canfar (they need -# VOSpace access); this script rebuilds from the extracted per-CCD corners and -# plots. Uncomment the marked lines to run the full chain. - -# Configuration variables - -## Catalogue versions -VERSIONS=("v1.3" "v1.4" "v1.5" "v1.6") - -## Output directory of plots -OUTPUT_DIR="coverage_map_plots" - -# healsparse resolution. nside=131072 gives ~0.1" per pixel, chosen to -# match the UNIONS bit-mask resolution so coverage and mask align pixel- -# wise. CoverageMapBuilder defaults to nside=2048 for lighter-weight -# offline use; override here for the production build. -BUILD_NSIDE=131072 -BUILD_CHANNELS=128 - -# Common parameters -VERBOSE="-v" - -## Colorbar -PLOT_COLORBAR="-C" -PLOT_MIN=1 -PLOT_MAX=5 - -# Plot parameters -## SGC region -SGC_RA_MIN=-20 -SGC_RA_MAX=45 -SGC_DEC_MIN=18 -SGC_DEC_MAX=40 - -## NGC region -NGC_RA_MIN=110 -NGC_RA_MAX=270 -NGC_DEC_MIN=28 -NGC_DEC_MAX=90 - -# Create output directory if it doesn't exist -mkdir -p "${OUTPUT_DIR}" - -# Loop over versions -for VERSION in "${VERSIONS[@]}"; do - echo "Processing ${VERSION}..." - - # Define file paths - CCD_LIST="ccds_with_psf_${VERSION}.txt" - HEADER_DIR="headers_${VERSION}" - INPUT_CORNERS="exp_ra_dec_${VERSION}.txt" - COVERAGE_MAP="coverage_${VERSION}.x.hsp" - - # Step 0-2: build the per-CCD corner file (canfar / VOSpace only). - # Uncomment to run the full chain from scratch. - # get_ccds_with_psf -V "${VERSION}" -o "${CCD_LIST}" ${VERBOSE} - # download_headers -i "${CCD_LIST}" -o "${HEADER_DIR}" ${VERBOSE} - # extract_field_corners -i "${HEADER_DIR}" -l "${CCD_LIST}" \ - # -o "${INPUT_CORNERS}" ${VERBOSE} - - # Build coverage map - echo " Building coverage map from ${INPUT_CORNERS}..." - CMD="build_coverage_map -i ${INPUT_CORNERS} -o ${COVERAGE_MAP} -c ${BUILD_CHANNELS} -n ${BUILD_NSIDE} ${VERBOSE}" - echo "$CMD" - $CMD - - # Plot SGC region - echo " Plotting SGC region..." - CMD="plot_coverage_map -i ${COVERAGE_MAP} -o ${OUTPUT_DIR}/coverage_${VERSION}_SGC.png ${VERBOSE} ${PLOT_COLORBAR} -R ${SGC_RA_MIN} -r ${SGC_RA_MAX} -D ${SGC_DEC_MIN} -d ${SGC_DEC_MAX} -m ${PLOT_MIN} -M ${PLOT_MAX}" - echo "$CMD" - $CMD - - # Plot NGC region - echo " Plotting NGC region..." - CMD="plot_coverage_map -i ${COVERAGE_MAP} -o ${OUTPUT_DIR}/coverage_${VERSION}_NGC.png ${VERBOSE} ${PLOT_COLORBAR} -R ${NGC_RA_MIN} -r ${NGC_RA_MAX} -D ${NGC_DEC_MIN} -d ${NGC_DEC_MAX} -m ${PLOT_MIN} -M ${PLOT_MAX}" - echo "$CMD" - $CMD - - echo " Done with ${VERSION}" - echo -done - -echo "All versions processed successfully!" diff --git a/src/shapepipe/coverage_run.py b/src/shapepipe/coverage_run.py index 15aaa1a37..49ca829fd 100644 --- a/src/shapepipe/coverage_run.py +++ b/src/shapepipe/coverage_run.py @@ -8,32 +8,11 @@ import sys -from shapepipe.utilities.header_downloader import HeaderDownloader from shapepipe.utilities.field_corners_extractor import FieldCornersExtractor from shapepipe.utilities.coverage_map_builder import CoverageMapBuilder from shapepipe.utilities.coverage_plotter import CoveragePlotter -def run_download_headers(args=None): - """Run Download Headers. - - Download FITS headers from VOSpace for exposures in a CCD list. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code - - """ - obj = HeaderDownloader() - return obj.run(args=args) - - def run_extract_corners(args=None): """Run Extract Corners. diff --git a/src/shapepipe/get_ccds_run.py b/src/shapepipe/get_ccds_run.py deleted file mode 100644 index fb18b0a74..000000000 --- a/src/shapepipe/get_ccds_run.py +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env python3 - -"""GET_CCDS_WITH_PSF - -Obtain list of CCDs (single-exposure single-HDU files) for which valid PSF information -is available. This can serve to create a footprint coverage mask. - -Author: Martin Kilbinger - -""" - -import sys - -from shapepipe.utilities.ccd_psf_handler import CcdPsfHandler - - -def run_ccd_psf_handler(args=None): - """Run CCD PSF Handler. - - Create instance and run the CCD PSF handler. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code - - """ - # Create instance - - obj = CcdPsfHandler() - - return obj.run(args=args) - - -def main(argv=None): - """Main. - """ - # A scripts to call the ccd psf class is created by pyproject.toml - return 0 diff --git a/src/shapepipe/utilities/ccd_psf_handler.py b/src/shapepipe/utilities/ccd_psf_handler.py deleted file mode 100644 index 2d0dc90eb..000000000 --- a/src/shapepipe/utilities/ccd_psf_handler.py +++ /dev/null @@ -1,387 +0,0 @@ -"""CCD_PSF_HANDLER - -Obtain list of CCDs (single-exposure single-HDU files) for which valid PSF -information is available. This can serve to create a footprint coverage mask. - -Author: Mike Hudson, Martin Kilbinger - -""" - -import glob -import os -import re -import sys - -import numpy as np - -from cs_util import args as cs_args -from cs_util import logging - -from shapepipe.utilities import summary - - -class CcdPsfHandler(object): - """CCD PSF Handler Class. - - Handles extraction of CCDs with valid PSF information from shapepipe - patches. - """ - - def __init__(self): - """Initialize the handler.""" - self.params_default() - - def params_default(self): - """Set default parameters and command line options.""" - - self._params = { - "version_cat": "v1.6", - "n_CCD": 40, - "output": None, - } - - self._short_options = { - "version_cat": "-V", - "n_CCD": "-n", - "output": "-o", - } - - self._types = { - "n_CCD": "int", - } - - self._help_strings = { - "version_cat": "catalogue major version, allowed are v1.3, v1.4, v1.5, v1.6; default is {}", - - "n_CCD": "number of CCDs per exposure; default is {}", - "output": "output file path; default is ccds_with_psf_.txt", - } - - def set_params_from_command_line(self, args): - """Set Params From Command line. - - Only use when calling using python from command line. - Does not work from ipython or jupyter. - - Parameters - ---------- - args : list - command line arguments - - """ - # Read command line options - options = cs_args.parse_options( - self._params, - self._short_options, - self._types, - self._help_strings, - args=args, - ) - self._params = options - - # Save calling command - logging.log_command(args) - - def update_params(self): - """Update parameters. - - Set derived parameters based on input parameters. - """ - # Determine number of patches based on version - version = self._params["version_cat"] - if version in ("v1.3", "v1.4"): - n_patch = 7 - elif version == "v1.5": - n_patch = 8 - elif version == "v1.6": - n_patch = 9 - else: - raise ValueError(f"Invalid version {version}") - - self._params["n_patch"] = n_patch - self._params["patches"] = [f"P{x}" for x in np.arange(n_patch) + 1] - - # Set output file if not specified - if self._params["output"] is None: - self._params["output"] = f"ccds_with_psf_{version}.txt" - - def check_params(self): - """Check parameters for validity.""" - # Add any parameter validation logic here - pass - - def get_lines(self, fname): - """Get Lines. - - Return list of lines read from a text file. - - Parameters - ---------- - fname : str - input file name - - Returns - ------- - list - IDs - - """ - IDs = [] - with open(fname) as f: - lines = f.readlines() - for line in lines: - IDs.append(line.rstrip()) - - return IDs - - def get_exp_shdu_missing(self, patches): - """Get Exp Shdu Missing. - - Returns set of missing CCDs (single-exposure single-HDU IDs) from a list of patches. - - Parameters - ---------- - patches : list - input patches - - Returns - ------- - set - missing CCD IDs - - """ - exp_shdu_missing_all = set() - - for patch in patches: - - path_exp_shdu_missing = f"{patch}/summary/missing_job_32_all.txt" - exp_shdu_missing = self.get_lines(path_exp_shdu_missing) - - print( - f"Patch {patch}: Found {len(exp_shdu_missing)} missing ccds", - end="; ", - ) - - exp_shdu_missing_all.update(exp_shdu_missing) - - print(f"cumulative {len(exp_shdu_missing_all)} missing ccds") - - print() - - return exp_shdu_missing_all - - def get_exp(self, patches): - """Get Exp. - - Return set of exposures from a list of patches. - - Parameters - ---------- - patches : list - input patches - - Returns - ------- - set - exposure IDs - - """ - exp_all = set() - - for patch in patches: - - path_exp = f"{patch}/exp_numbers.txt" - exp = self.get_lines(path_exp) - - print(f"Patch {patch}: Found {len(exp)} exposures", end="; ") - - exp_all.update(exp) - - print(f"cumulative {len(exp_all)} exposures") - - print() - - return exp_all - - def get_ccds_with_psf(self, patches, n_CCD=40): - """Get CCDs With PSF. - - Return set of CCDs with valid PSF from a list of patches. A CCD has a - valid PSF if it appears among the exposures' single-HDU IDs but not in - the per-patch missing-CCD lists. - - Parameters - ---------- - patches : list - input patches - n_CCD : int - number of CCDs per exposure - - Returns - ------- - set - CCD IDs with valid PSF - - """ - # Get missing CCDs - print("=== get missing CCDs ===") - exp_shdu_missing_all = self.get_exp_shdu_missing(patches) - - # Get all exposures used in tiles - print("=== get exposures ===") - exp_all = self.get_exp(patches) - - # Turn exposures into exposure-single-HDU names (CCDs) - exp_shdu_all = set(summary.get_all_shdus(exp_all, n_CCD)) - - # Subtract the CCDs whose PSF model is missing - exp_shdu_with_psf = exp_shdu_all - exp_shdu_missing_all - - print( - f"Found {len(exp_shdu_all)} CCDs, " - f"{len(exp_shdu_missing_all)} missing, " - f"{len(exp_shdu_with_psf)} with valid PSF" - ) - - return exp_shdu_with_psf - - def get_ccds_with_psf_method_v1_3(self, patches, n_CCD=40): - """Get CCDs With PSF Method v1.3. - - Return set of CCDs with valid PSF by scanning run directories. - Finds star_selection files and checks corresponding stat files - for valid (non-nan) FWHM values. - - Parameters - ---------- - patches : list - input patches - n_CCD : int - number of CCDs per exposure (unused, kept for API consistency) - - Returns - ------- - set - CCD IDs with valid PSF - - """ - exp_shdu_all = set() - - for patch in patches: - # Find all star_selection files - mask_pattern = ( - f"{patch}/output/run_sp_exp_SxSePs*/" - f"setools_runner/output/mask/star_selection-*.fits" - ) - mask_files = glob.glob(mask_pattern) - - print( - f"Patch {patch}: Found {len(mask_files)} star_selection FITS" - + " files" - ) - - for mask_file in mask_files: - # Extract exp_shdu from filename - basename = os.path.basename(mask_file) - match = re.match(r"star_selection-(.+)\.fits", basename) - if not match: - print(f"Warning: Non-matching file name {basename}") - continue - exp_shdu = match.group(1) - - # Get corresponding stat file path - # Replace mask dir with stat/stat dir in the path - stat_file = mask_file.replace( - "/mask/star_selection-", - "/stat/star_stat-", - ).replace(".fits", ".txt") - - #print("MKDEBUG ", exp_shdu, stat_file) - - # Check if stat file exists and has valid FWHM - if not os.path.exists(stat_file): - print("MKDEBUG stat file ", stat_file, " does not exist") - continue - - # Read stat file and check for nan in "Mean star fwhm selected" - has_nan = False - with open(stat_file) as f: - for line in f: - if "Mean star fwhm selected" in line: - if "nan" in line.lower(): - has_nan = True - break - - if not has_nan: - #print("MKDEBUG append", exp_shdu) - exp_shdu_all.add(exp_shdu) - - print(f"After patch {patch}: {len(exp_shdu_all)} CCDs with valid PSF") - - print(f"Found {len(exp_shdu_all)} CCDs with valid PSF") - - return exp_shdu_all - - def save(self, IDs, path): - """Save. - - Save list of IDs to text file. - - Parameters - ---------- - IDs : set - input IDs - path : str - output file name - - """ - with open(path, "w") as f_out: - for ID in IDs: - print(ID, file=f_out) - - def run(self, args=None): - """Run. - - Main execution method. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code (0 for success) - - """ - if args is None: - args = sys.argv[1:] - - # Set parameters from command line - self.set_params_from_command_line(args) - self.update_params() - self.check_params() - - # Get parameters - patches = self._params["patches"] - version = self._params["version_cat"] - n_CCD = self._params["n_CCD"] - output = self._params["output"] - - print( - f"=== get_ccds_with_psf for version {version}, patches {patches} ===" - ) - - if self._params["version_cat"] == "v1.3": - print("=== method for 1.3: exp with PSF star sample === ") - exp_shdu_all = self.get_ccds_with_psf_method_v1_3(patches, n_CCD) - else: - print("=== method for >= 1.4: exp_list - missing === ") - exp_shdu_all = self.get_ccds_with_psf(patches, n_CCD) - - self.save(exp_shdu_all, output) - - print(f"Results saved to {output}") - - return 0 diff --git a/src/shapepipe/utilities/header_downloader.py b/src/shapepipe/utilities/header_downloader.py deleted file mode 100644 index 122327a58..000000000 --- a/src/shapepipe/utilities/header_downloader.py +++ /dev/null @@ -1,291 +0,0 @@ -"""HEADER_DOWNLOADER - -Download FITS headers from VOSpace for exposures listed in a CCD file. - -Author: Mike Hudson, Martin Kilbinger - -""" - -import sys -import os -from os.path import exists - -import numpy as np -import vos - -from cs_util import args as cs_args -from cs_util import logging - - -class HeaderDownloader(object): - """Header Downloader Class. - - Downloads FITS headers from VOSpace for exposures in a CCD list. - """ - - def __init__(self): - """Initialize the downloader.""" - self.params_default() - - def params_default(self): - """Set default parameters and command line options.""" - - self._params = { - "input_file": None, - "output_dir": "header", - "vospace_path": "vos:cfis/pitcairn", # UNIONS/CFIS default; override for other surveys - "overwrite": False, - "dir_for_links": None, - "verbose": False, - } - - self._short_options = { - "input_file": "-i", - "output_dir": "-o", - "vospace_path": "-p", - "overwrite": "-O", - "dir_for_links": "-d", - } - - self._types = { - "overwrite": "bool", - "verbose": "bool", - } - - self._help_strings = { - "input_file": "input CCD list file (txt or csv); required", - "output_dir": "output directory for headers; default is {}", - "vospace_path": "VOSpace base path; default is {}", - "overwrite": "overwrite existing header files; default is {}", - "dir_for_links": "directory to check for existing headers to link instead of download; default is {}", - } - - def set_params_from_command_line(self, args): - """Set Params From Command line. - - Only use when calling using python from command line. - Does not work from ipython or jupyter. - - Parameters - ---------- - args : list - command line arguments - - """ - # Read command line options - options = cs_args.parse_options( - self._params, - self._short_options, - self._types, - self._help_strings, - args=args, - ) - self._params = options - - # Save calling command - logging.log_command(args) - - def update_params(self): - """Update parameters. - - Set derived parameters based on input parameters. - """ - # Ensure output directory ends without trailing slash - if self._params["output_dir"].endswith("/"): - self._params["output_dir"] = self._params["output_dir"][:-1] - - def check_params(self): - """Check parameters for validity.""" - if self._params["input_file"] is None: - raise ValueError("Input file is required (use -i or --input_file)") - - if not exists(self._params["input_file"]): - raise FileNotFoundError( - f"Input file not found: {self._params['input_file']}" - ) - - # Check if dir_for_links exists if specified - if self._params["dir_for_links"] is not None: - if not exists(self._params["dir_for_links"]): - raise FileNotFoundError( - f"Directory for links not found: {self._params['dir_for_links']}" - ) - if not os.path.isdir(self._params["dir_for_links"]): - raise ValueError( - f"Path is not a directory: {self._params['dir_for_links']}" - ) - - # Create output directory if it doesn't exist - if not exists(self._params["output_dir"]): - os.makedirs(self._params["output_dir"]) - if self._params["verbose"]: - print(f"Created output directory: {self._params['output_dir']}") - - def get_exposures(self, ccd_list_file): - """Get Exposures. - - Extract unique exposure numbers from CCD list file. - - Parameters - ---------- - ccd_list_file : str - path to CCD list file (txt or csv) - - Returns - ------- - np.array - unique exposure numbers - - """ - exps = [] - - # Check if CSV format - if ccd_list_file.endswith(".csv"): - import astropy.table - - t = astropy.table.Table.read(ccd_list_file) - expccd = t["CCD"].data - r = np.char.split(expccd, sep="-") - - for i, r1 in enumerate(r): - exp = int(r1[0]) - exps.append(exp) - else: - # Text format; atleast_1d so a single-line file (0-d array) is - # still iterable. - f = np.atleast_1d( - np.loadtxt(ccd_list_file, dtype="str", encoding="ascii") - ) - r = np.char.split(f, sep="-") - - for i, r1 in enumerate(r): - exp = int(r1[0]) - exps.append(exp) - - exps = np.array(exps) - uniq = np.unique(exps) - - return uniq - - def get_fits_header(self, expnum, client): - """Get FITS Header. - - Download FITS header from VOSpace, or create a symbolic link - if the header exists in dir_for_links. - - Parameters - ---------- - expnum : int - exposure number - client : vos.Client - VOSpace client - - Returns - ------- - bool - True if successful, False otherwise - - """ - vospace_path = self._params["vospace_path"] - output_dir = self._params["output_dir"] - overwrite = self._params["overwrite"] - dir_for_links = self._params["dir_for_links"] - - source = f"{vospace_path}/{expnum:d}p.fits.fz" - dest = f"{output_dir}/{expnum:d}.txt" - - if exists(dest) and not overwrite: - return True - - # Check if header exists in dir_for_links - if dir_for_links is not None: - link_source = os.path.abspath(f"{dir_for_links}/{expnum:d}.txt") - if exists(link_source): - try: - # Remove existing file/link if overwrite is True - if exists(dest): - os.remove(dest) - # Create symbolic link - os.symlink(link_source, dest) - return True - except Exception as e: - print(f"Could not create symlink from {link_source}: {e}") - # Fall through to download if symlink fails - - # Download from VOSpace atomically: copy to a temp file in the same - # directory, then rename on success. An interrupted transfer leaves - # only the temp file behind, so resume never treats a partial download - # as complete. - tmp_dest = f"{dest}.part" - try: - client.copy(source, tmp_dest, head=True) - os.rename(tmp_dest, dest) - return True - except Exception as e: - print(f"Could not copy {source}: {e}") - if exists(tmp_dest): - os.remove(tmp_dest) - return False - - def run(self, args=None): - """Run. - - Main execution method. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code (0 for success) - - """ - if args is None: - args = sys.argv[1:] - - # Set parameters from command line - self.set_params_from_command_line(args) - self.update_params() - self.check_params() - - # Get parameters - input_file = self._params["input_file"] - output_dir = self._params["output_dir"] - verbose = self._params["verbose"] - - if verbose: - print(f"Reading CCD list from: {input_file}") - - # Extract unique exposures - exps = self.get_exposures(input_file) - - print(f"Found {len(exps)} unique exposures") - - if verbose: - print(f"Downloading headers to: {output_dir}/") - - # Initialize VOSpace client once - client = vos.Client() - - # Download headers - n_success = 0 - n_failed = 0 - - for i, exp in enumerate(exps): - success = self.get_fits_header(exp, client) - if success: - n_success += 1 - else: - n_failed += 1 - - if verbose and i % 100 == 0: - print(f"{i:6d} / {len(exps):6d}") - - print(f"Downloaded {n_success} headers") - if n_failed > 0: - print(f"Failed to download {n_failed} headers") - - return 0 diff --git a/tests/module/test_coverage.py b/tests/module/test_coverage.py index ea48905c3..8cbe8e743 100644 --- a/tests/module/test_coverage.py +++ b/tests/module/test_coverage.py @@ -1,15 +1,13 @@ """UNIT / PROPERTY TESTS FOR THE COVERAGE-MASK FEATURE. Covers the pure and lightly-fixtured logic behind the per-CCD coverage nexp -masks: exposure-number parsing, the CCD-list -> unique-exposure reduction, the -handler's missing-CCD subtraction (pinned against the real ID format), per-CCD -corner extraction from plain and fpack-compressed multi-HDU headers, -``--ccd_list`` filtering, the per-CCD resume path, the builder's per-CCD row -parsing, the accumulated exposure-count (nexp) contract, the RA-wrap and pole -guards, the power-of-two ``nside`` validation, the atomic-download rename, and -the ``-h``/argv handling of the console runners. The heavy paths (real VOSpace -download, production-resolution map building, plotting, multiprocessing) are -exercised end to end by the pipeline, not here. +masks: exposure-number parsing, per-CCD corner extraction from plain and +fpack-compressed multi-HDU headers, ``--ccd_list`` filtering, the per-CCD +resume path, the builder's per-CCD row parsing, the accumulated +exposure-count (nexp) contract, the RA-wrap and pole guards, the power-of-two +``nside`` validation, and the ``-h``/argv handling of the console runners. The +heavy paths (production-resolution map building, plotting, multiprocessing) +are exercised end to end by the pipeline, not here. """ import sys @@ -22,8 +20,6 @@ from hypothesis import given from hypothesis import strategies as st -from shapepipe.utilities import summary -from shapepipe.utilities.ccd_psf_handler import CcdPsfHandler from shapepipe.utilities.coverage_map_builder import ( CoverageMapBuilder, unwrap_ra, @@ -35,7 +31,6 @@ _image_shape, _parse_header_to_wcs, ) -from shapepipe.utilities.header_downloader import HeaderDownloader def _tan_wcs(crval_ra, crval_dec=0.0, nx=2080, ny=4612): @@ -126,138 +121,6 @@ def test_expnum_from_path_roundtrips(expnum): assert _expnum_from_path(f"vos_headers/{expnum}.txt") == expnum -# --------------------------------------------------------------------------- -# HeaderDownloader.get_exposures -# --------------------------------------------------------------------------- - -def test_get_exposures_reduces_to_unique_exposures(tmp_path): - """A ``-`` CCD list collapses to its unique exposure numbers.""" - ccd_list = tmp_path / "ccds.txt" - ccd_list.write_text("2143523-0\n2143523-5\n2143524-3\n2143524-8\n") - - exps = HeaderDownloader().get_exposures(str(ccd_list)) - - npt.assert_array_equal(exps, np.array([2143523, 2143524])) - - -def test_get_exposures_single_line(tmp_path): - """A one-line CCD list (0-d loadtxt array) still yields one exposure.""" - ccd_list = tmp_path / "ccds.txt" - ccd_list.write_text("2143523-0\n") - - exps = HeaderDownloader().get_exposures(str(ccd_list)) - - npt.assert_array_equal(exps, np.array([2143523])) - - -def test_get_exposures_csv_matches_txt(tmp_path): - """The CSV and text code paths yield the same unique exposures.""" - txt = tmp_path / "ccds.txt" - txt.write_text("2143523-0\n2143523-5\n2143524-3\n") - csv = tmp_path / "ccds.csv" - csv.write_text("CCD\n2143523-0\n2143523-5\n2143524-3\n") - - dl = HeaderDownloader() - - npt.assert_array_equal( - dl.get_exposures(str(txt)), dl.get_exposures(str(csv)) - ) - - -# --------------------------------------------------------------------------- -# HeaderDownloader.get_fits_header — atomic rename -# --------------------------------------------------------------------------- - -def test_get_fits_header_writes_atomically(tmp_path): - """A successful download copies to ``.part`` then renames to the dest.""" - dl = HeaderDownloader() - dl._params["output_dir"] = str(tmp_path) - dl._params["overwrite"] = False - dl._params["dir_for_links"] = None - - dest = tmp_path / "42.txt" - tmp_dest = tmp_path / "42.txt.part" - - def fake_copy(source, target, head=True): - # The copy must land on the temp path, not the final destination. - assert target == str(tmp_dest) - with open(target, "w") as f: - f.write("HEADER") - - client = SimpleNamespace(copy=fake_copy) - - assert dl.get_fits_header(42, client) is True - assert dest.exists() - assert not tmp_dest.exists() - assert dest.read_text() == "HEADER" - - -def test_get_fits_header_failed_copy_leaves_no_dest(tmp_path): - """A failed download leaves no destination file (only, if any, ``.part``).""" - dl = HeaderDownloader() - dl._params["output_dir"] = str(tmp_path) - dl._params["overwrite"] = False - dl._params["dir_for_links"] = None - - def failing_copy(source, target, head=True): - raise RuntimeError("transfer interrupted") - - client = SimpleNamespace(copy=failing_copy) - - assert dl.get_fits_header(42, client) is False - assert not (tmp_path / "42.txt").exists() - assert not (tmp_path / "42.txt.part").exists() - - -# --------------------------------------------------------------------------- -# CcdPsfHandler.get_ccds_with_psf — missing-CCD subtraction -# --------------------------------------------------------------------------- - -def test_get_ccds_with_psf_subtracts_missing(monkeypatch): - """Valid CCDs are all exposure single-HDUs minus the missing set. - - The real ``summary.get_all_shdus`` is used so the cross-component - ``-`` ID format is pinned end to end. - """ - handler = CcdPsfHandler() - - # Two exposures, 3 CCDs each -> 6 candidate CCDs; two are missing. - monkeypatch.setattr(handler, "get_exp", lambda patches: {"100", "200"}) - monkeypatch.setattr( - handler, - "get_exp_shdu_missing", - lambda patches: {"100-1", "200-2"}, - ) - - result = handler.get_ccds_with_psf(["P1"], n_CCD=3) - - # get_all_shdus yields "-" for ccd in range(n_CCD). - assert result == {"100-0", "100-2", "200-0", "200-1"} - # Guard the assumption that the missing IDs share the produced format. - assert set(summary.get_all_shdus({"100"}, 3)) == {"100-0", "100-1", "100-2"} - - -@pytest.mark.parametrize( - ("version", "n_patch"), - [("v1.3", 7), ("v1.4", 7), ("v1.5", 8), ("v1.6", 9)], -) -def test_version_to_patch_count(version, n_patch): - """Each v1.x catalogue version maps to its patch count.""" - handler = CcdPsfHandler() - handler._params["version_cat"] = version - handler.update_params() - assert handler._params["n_patch"] == n_patch - assert len(handler._params["patches"]) == n_patch - - -def test_invalid_version_raises(): - """An unknown catalogue version fails loudly.""" - handler = CcdPsfHandler() - handler._params["version_cat"] = "v9.9" - with pytest.raises(ValueError, match="v9.9"): - handler.update_params() - - # --------------------------------------------------------------------------- # image-shape resolution (fpack ZNAXIS vs plain NAXIS) # --------------------------------------------------------------------------- From 1ac7f90290723101b54b2f4fd78a551872e540d9 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:23:25 +0200 Subject: [PATCH 19/36] refactor(coverage): keep the geometry, drop the file-scraping CLI around it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit What the coverage chain was worth keeping is four pieces of survey geometry: the CCD image shape (fpack ZNAXIS vs plain NAXIS), the pixel-edge corner projection, the RA-seam and pole guards, and the nside validation. Everything around them was a way of getting header text files off VOSpace and per-CCD rows into a shared exp_ra_dec.txt — a shape that exists because the pre-Snakemake pipeline kept no per-unit record. The workflow feeds arrays out of the per-exposure exp_footprint.json records instead, so the text format, the resumable directory scrape and the CLIs that drove them have no producer. - field_corners_extractor.py -> ccd_footprint.py, 493 -> 92 lines: the two pure functions the exp_footprint rule calls, and nothing else. - coverage_map_builder.py loses the CoverageMapBuilder class: the text loader, the boolean-threshold derivation and the create_plot branch (plotting has its own entry point and stays out of the DAG). unwrap_ra and build_map are unchanged; check_params' power-of-two validation becomes check_nside, called by build_map so it protects the caller rather than the CLI; median_filter becomes a module function and grows its iteration loop. - coverage_run.py keeps run_plot_coverage alone. extract_field_corners and build_coverage_map leave [project.scripts] in the same commit, per test_entrypoints.py. test_coverage.py is rebuilt around the surviving functions: the header tests construct Headers directly instead of round-tripping through the retired text parser, and the map tests call build_map instead of writing a corners file and running the CLI. The nexp contract, the RA-shift invariance guard and the pole guard are unchanged in substance. Building the map no longer writes a stray `log_-i` into the working tree, since cs_util's log_command went with the CLI. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- pyproject.toml | 2 - src/shapepipe/coverage_run.py | 62 +-- src/shapepipe/utilities/ccd_footprint.py | 92 ++++ .../utilities/coverage_map_builder.py | 351 ++----------- .../utilities/field_corners_extractor.py | 493 ------------------ tests/module/test_coverage.py | 485 ++++------------- workflow/README.md | 9 +- workflow/config.yaml | 12 +- workflow/scripts/coverage_map.py | 22 +- workflow/scripts/exp_footprint.py | 2 +- 10 files changed, 277 insertions(+), 1253 deletions(-) create mode 100644 src/shapepipe/utilities/ccd_footprint.py delete mode 100644 src/shapepipe/utilities/field_corners_extractor.py diff --git a/pyproject.toml b/pyproject.toml index 1010b4b47..b400d618f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,8 +88,6 @@ summary_run = "shapepipe.summary_run:main" canfar_submit_job = "shapepipe.canfar_run:run_job" canfar_monitor = "shapepipe.canfar_run:run_log" canfar_monitor_log = "shapepipe.canfar_run:run_monitor_log" -extract_field_corners = "shapepipe.coverage_run:run_extract_corners" -build_coverage_map = "shapepipe.coverage_run:run_build_coverage" plot_coverage_map = "shapepipe.coverage_run:run_plot_coverage" [tool.uv] diff --git a/src/shapepipe/coverage_run.py b/src/shapepipe/coverage_run.py index 49ca829fd..9a167d8d6 100644 --- a/src/shapepipe/coverage_run.py +++ b/src/shapepipe/coverage_run.py @@ -1,58 +1,20 @@ """COVERAGE_RUN -Call coverage processing classes. +Console entry point for plotting a coverage map. + +Building the map is the Snakemake ``coverage_map`` rule's job; plotting a +finished ``.hsp`` is a human act on a durable product, so it stays a +hand-run command, on the same argument that keeps ``run_report.py`` out of the +DAG. The plot windows for the UNIONS SGC and NGC fields are in +``workflow/config.yaml``'s ``coverage:`` block. Author: Martin Kilbinger """ -import sys - -from shapepipe.utilities.field_corners_extractor import FieldCornersExtractor -from shapepipe.utilities.coverage_map_builder import CoverageMapBuilder from shapepipe.utilities.coverage_plotter import CoveragePlotter -def run_extract_corners(args=None): - """Run Extract Corners. - - Extract per-CCD sky-footprint corner coordinates from FITS headers. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code - - """ - obj = FieldCornersExtractor() - return obj.run(args=args) - - -def run_build_coverage(args=None): - """Run Build Coverage. - - Build HealSparse coverage maps from per-CCD corner coordinates. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code - - """ - obj = CoverageMapBuilder() - return obj.run(args=args) - - def run_plot_coverage(args=None): """Run Plot Coverage. @@ -71,13 +33,3 @@ def run_plot_coverage(args=None): """ obj = CoveragePlotter() return obj.run(args=args) - - -def main(argv=None): - """Main. - - Main program. - - """ - # Scripts to call coverage classes are created by pyproject.toml - return 0 diff --git a/src/shapepipe/utilities/ccd_footprint.py b/src/shapepipe/utilities/ccd_footprint.py new file mode 100644 index 000000000..41cff7612 --- /dev/null +++ b/src/shapepipe/utilities/ccd_footprint.py @@ -0,0 +1,92 @@ +"""CCD_FOOTPRINT + +Project a single CCD onto the sky: image shape from a header, sky corners +from a WCS. + +The two functions here are the geometry the coverage mask rests on, and they +are all that survives of the pre-Snakemake header scrape. The ``exp_footprint`` +rule (``workflow/scripts/exp_footprint.py``) calls them once per CCD of an +exposure, reading the headers out of ``headers-.npy`` rather than out of +text files downloaded from VOSpace; the ``coverage_map`` rule then stamps the +corners into the campaign's HealSparse map. + +Author: Mike Hudson, Martin Kilbinger + +""" + + +def _image_shape(header): + """Return the CCD image ``(nx, ny)`` pixel shape from a header. + + For fpack tile-compressed HDUs (``ZIMAGE = T``), ``NAXIS1``/``NAXIS2`` + describe the compressed *binary table* (row width in bytes, row count), + not the image, and astropy's WCS never maps the true dimensions into + ``pixel_shape``. The true image size is carried by ``ZNAXIS1``/``ZNAXIS2``, + which are preferred here; a plain image HDU falls back to + ``NAXIS1``/``NAXIS2``. + + Both branches are live. ``headers-.npy`` carries the *decompressed* + header astropy hands back for a tile-compressed HDU, so it takes the plain + ``NAXIS`` path; a header read straight off a fpacked file takes the ``Z*`` + one. + + Parameters + ---------- + header : astropy.io.fits.Header + single-HDU header + + Returns + ------- + tuple + ``(nx, ny)`` image pixel dimensions + + Raises + ------ + ValueError + if neither the ``Z*`` nor the plain ``NAXIS`` dimensions are present + """ + if header.get("ZIMAGE", False): + keys = ("ZNAXIS1", "ZNAXIS2") + else: + keys = ("NAXIS1", "NAXIS2") + + if keys[0] not in header or keys[1] not in header: + raise ValueError( + f"Header is missing image dimensions ({keys[0]}/{keys[1]})" + ) + + return int(header[keys[0]]), int(header[keys[1]]) + + +def _ccd_corners(w, shape): + """Return (ra, dec) of the 4 corners of a single CCD. + + The polygon is built from the CCD's pixel bounds ``shape = (nx, ny)`` using + pixel edges ``(-0.5, n - 0.5)`` so the quadrilateral covers the full CCD + area rather than pixel centres. Corners are returned in a consistent + counterclockwise pixel order (bottom-left, bottom-right, top-right, + top-left); since ``pixel_to_world`` maps this order to a simple, + non-self-intersecting sky quadrilateral for a CCD-sized field, and + HealSparse ``Polygon`` is orientation-agnostic, the resulting polygon is a + valid convex footprint. + + Parameters + ---------- + w : astropy.wcs.WCS + WCS of a single CCD + shape : tuple + ``(nx, ny)`` CCD image pixel dimensions + + Returns + ------- + tuple + ``(ra_list, dec_list)`` each of length 4, in degrees + """ + nx, ny = shape + + # Pixel-edge corners, counterclockwise: BL, BR, TR, TL + px = [-0.5, nx - 0.5, nx - 0.5, -0.5] + py = [-0.5, -0.5, ny - 0.5, ny - 0.5] + + sky = w.pixel_to_world(px, py) + return list(sky.ra.deg), list(sky.dec.deg) diff --git a/src/shapepipe/utilities/coverage_map_builder.py b/src/shapepipe/utilities/coverage_map_builder.py index ffa2471fe..43584c94e 100644 --- a/src/shapepipe/utilities/coverage_map_builder.py +++ b/src/shapepipe/utilities/coverage_map_builder.py @@ -1,26 +1,28 @@ """COVERAGE_MAP_BUILDER -Build HealSparse coverage maps from per-CCD corner coordinates. +Stamp per-CCD sky footprints into a HealSparse coverage (nexp) map. -Each input row is one CCD footprint. Since the CCDs of a single exposure do -not overlap, stamping value 1 per CCD polygon and accumulating makes the map -count, per sky pixel, the number of exposures with a valid PSF model covering -that pixel. +Each polygon is one CCD footprint. Since the CCDs of a single exposure do not +overlap, stamping value 1 per CCD polygon and accumulating makes the map count, +per sky pixel, the number of exposures with a valid PSF model covering that +pixel. + +Arrays in, map out. The only caller is the ``coverage_map`` rule +(``workflow/scripts/coverage_map.py``), which reads the corners out of the +per-exposure ``exp_footprint.json`` records; the nine-column ``exp_ra_dec.txt`` +the pre-Snakemake chain appended to is gone, and so is the CLI that parsed it. +What is left here is survey geometry — the RA-seam guard, the pole guard, the +nside validation and the median smoothing — none of which depends on how the +corners arrived. Author: Mike Hudson, Martin Kilbinger """ -import sys -from os.path import exists - import numpy as np import healsparse as hsp import hpgeom as hpg -from cs_util import args as cs_args -from cs_util import logging - # Declination beyond which a polygon is considered too close to a pole for # HealSparse's planar polygon fill to be reliable. CCD footprints are ~10 # arcmin, so this is a guard against pathological headers, not a common path. @@ -52,32 +54,29 @@ def unwrap_ra(ra): return ra -def load_corners(input_file): - """Read per-CCD corners from an ``exp_ra_dec.txt``. +def check_nside(nside_coverage, nside): + """Raise unless both HealSparse resolutions are positive powers of two. - The nine-column text format the pre-Snakemake chain appended to: column - 0 is a ``-`` string ID, the remaining eight are the 4 RA - then the 4 Dec corners. The Snakemake workflow feeds :func:`build_map` - arrays directly instead (one JSON record per exposure, written by the - ``exp_footprint`` rule), so this loader exists only for the CLI. + Checked before any polygon is stamped: healsparse itself complains only + once a map is being made, by which time the caller has already paid for + reading every footprint record on the products root. Parameters ---------- - input_file : str - path to the per-CCD corner file + nside_coverage : int + HealSparse coverage nside + nside : int + HealSparse map nside - Returns - ------- - tuple - ``(ccd_ids, ra, dec)``: a length-N string array and two ``(N, 4)`` - float arrays, in degrees + Raises + ------ + ValueError + if either value is not a positive power of two """ - ccd_ids = np.atleast_1d(np.loadtxt(input_file, usecols=(0), dtype=str)) - corners = np.atleast_2d( - np.loadtxt(input_file, usecols=range(1, 9), dtype=float) - ) - return ccd_ids, corners[:, :4], corners[:, 4:] + for name, n in (("nside_coverage", nside_coverage), ("nside", nside)): + if n <= 0 or n & (n - 1): + raise ValueError(f"{name} must be a power of 2, got {n}") def build_map( @@ -85,14 +84,6 @@ def build_map( ): """Stamp one polygon per CCD footprint into a HealSparse nexp map. - The array entry point, and the one both callers go through: the - ``build_coverage_map`` CLI (which reads its arrays out of a text file - with :func:`load_corners`) and the Snakemake ``coverage_map`` rule (which - reads them out of the per-exposure ``exp_footprint.json`` records). The - stamping stays in one place so the two ways of getting the same map cannot - drift — the RA-seam and pole guards below are survey geometry, not - input-format handling. - Since the CCDs of a single exposure do not overlap, accumulating value 1 per CCD polygon makes the map count, per sky pixel, the number of exposures with a valid PSF model covering it. @@ -118,6 +109,8 @@ def build_map( the nexp map """ + check_nside(nside_coverage, nside) + ra = np.atleast_2d(np.asarray(ra, dtype=float)) dec = np.atleast_2d(np.asarray(dec, dtype=float)) @@ -162,161 +155,32 @@ def build_map( return m -class CoverageMapBuilder(object): - """Coverage Map Builder Class. - - Builds HealSparse coverage maps from field corner coordinates. - """ - - def __init__(self): - """Initialize the builder.""" - self.params_default() - - def params_default(self): - """Set default parameters and command line options.""" - - self._params = { - "input_file": "exp_ra_dec.txt", - "output_file": "coverage.hsp", - "nside_coverage": 32, - "nside": 2048, - "apply_median_filter": False, - "n_median_iterations": 2, - "create_boolean": False, - "boolean_threshold": 3, - "boolean_output": None, - "create_plot": False, - "plot_output": None, - "plot_region": None, - "verbose": False, - } - - self._short_options = { - "input_file": "-i", - "output_file": "-o", - "nside_coverage": "-c", - "nside": "-n", - "apply_median_filter": "-m", - "n_median_iterations": "-N", - "create_boolean": "-b", - "boolean_threshold": "-t", - "boolean_output": "-B", - "create_plot": "-p", - "plot_output": "-P", - "plot_region": "-g", - } - - self._types = { - "nside_coverage": "int", - "nside": "int", - "apply_median_filter": "bool", - "n_median_iterations": "int", - "create_boolean": "bool", - "boolean_threshold": "int", - "create_plot": "bool", - "verbose": "bool", - } - - self._help_strings = { - "input_file": "input file with field corners; default is {}", - "output_file": "output HealSparse map file; default is {}", - "nside_coverage": "HealSparse coverage nside; default is {}", - "nside": "HealSparse map nside; default is {}", - "apply_median_filter": "apply median filter to smooth map; default is {}", - "n_median_iterations": "number of median filter iterations; default is {}", - "create_boolean": "create boolean coverage map; default is {}", - "boolean_threshold": "threshold value for boolean map; default is {}", - "boolean_output": "output file for boolean map; default is _bool.hsp", - "create_plot": "create plot of coverage map; default is {}", - "plot_output": "output file for plot; default is .png", - "plot_region": "predefined region for plot (NGC, SGC, fullsky); default is {}", - } - - def set_params_from_command_line(self, args): - """Set Params From Command line. - - Only use when calling using python from command line. - Does not work from ipython or jupyter. - - Parameters - ---------- - args : list - command line arguments - - """ - # Read command line options - options = cs_args.parse_options( - self._params, - self._short_options, - self._types, - self._help_strings, - args=args, - ) - self._params = options - - # Save calling command - logging.log_command(args) - - def update_params(self): - """Update parameters. - - Set derived parameters based on input parameters. - """ - # Set boolean output filename if not specified - if self._params["boolean_output"] is None: - output_file = self._params["output_file"] - if output_file.endswith(".hsp"): - base = output_file[:-4] - else: - base = output_file - self._params["boolean_output"] = f"{base}_bool.hsp" - - # Set plot output filename if not specified - if self._params["plot_output"] is None: - output_file = self._params["output_file"] - if output_file.endswith(".hsp"): - base = output_file[:-4] - else: - base = output_file - self._params["plot_output"] = f"{base}.png" - - def check_params(self): - """Check parameters for validity.""" - if not exists(self._params["input_file"]): - raise FileNotFoundError( - f"Input file not found: {self._params['input_file']}" - ) - - # Check nside values are powers of 2 - nside_coverage = self._params["nside_coverage"] - nside = self._params["nside"] - - if not (nside_coverage & (nside_coverage - 1) == 0): - raise ValueError( - f"nside_coverage must be a power of 2, got {nside_coverage}" - ) - - if not (nside & (nside - 1) == 0): - raise ValueError(f"nside must be a power of 2, got {nside}") - - def median_filter(self, hsp_map): - """Median Filter. +def median_filter(hsp_map, n_iterations=1): + """Smooth an nexp map by replacing each pixel with its neighbourhood median. - Apply median filter to HealSparse map using neighbors. + The neighbourhood is a pixel and its eight HEALPix neighbours; out-of-map + neighbours read as the map's sentinel and so pull an edge pixel down, which + is the intended behaviour for a coverage map (a lone pixel is noise, not + depth). The ``coverage_map`` rule does NOT smooth — the map it writes is the + raw exposure count, which is what sp_validation's mask application wants — + so this is the offline step, for looking at a map rather than applying one. - Parameters - ---------- - hsp_map : healsparse.HealSparseMap - input map + Parameters + ---------- + hsp_map : healsparse.HealSparseMap + input nexp map + n_iterations : int, optional + number of smoothing passes - Returns - ------- - healsparse.HealSparseMap - filtered map + Returns + ------- + healsparse.HealSparseMap + filtered map - """ - nside = hsp_map.nside_sparse + """ + nside = hsp_map.nside_sparse + for _ in range(n_iterations): new_hsp = hsp_map.copy() pixs = hsp_map.valid_pixels n = hpg.neighbors(nside, pixs) @@ -326,113 +190,6 @@ def median_filter(self, hsp_map): mn = hsp_map[n] new = np.median(mn, axis=1).astype(np.uint16) new_hsp[pixs] = new + hsp_map = new_hsp - return new_hsp - - def run(self, args=None): - """Run. - - Main execution method. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code (0 for success) - - """ - if args is None: - args = sys.argv[1:] - - # Set parameters from command line - self.set_params_from_command_line(args) - self.update_params() - self.check_params() - - # Get parameters - input_file = self._params["input_file"] - output_file = self._params["output_file"] - nside_coverage = self._params["nside_coverage"] - nside = self._params["nside"] - apply_median_filter = self._params["apply_median_filter"] - n_median_iterations = self._params["n_median_iterations"] - create_boolean = self._params["create_boolean"] - boolean_threshold = self._params["boolean_threshold"] - boolean_output = self._params["boolean_output"] - create_plot = self._params["create_plot"] - plot_output = self._params["plot_output"] - plot_region = self._params["plot_region"] - verbose = self._params["verbose"] - - if verbose: - print(f"Reading per-CCD corners from: {input_file}") - - ccd_ids, ra_all, dec_all = load_corners(input_file) - - print(f"Loaded {len(ccd_ids)} CCD footprints") - - m = build_map( - ccd_ids, ra_all, dec_all, nside_coverage, nside, verbose=verbose - ) - - # Apply median filter if requested - if apply_median_filter: - if verbose: - print( - f"Applying median filter ({n_median_iterations} iterations)" - ) - - for i in range(n_median_iterations): - m = self.median_filter(m) - if verbose: - print(f" Iteration {i+1}/{n_median_iterations} complete") - - # Write main coverage map - if verbose: - print(f"Writing coverage map to: {output_file}") - - m.write(output_file, clobber=True) - print(f"Coverage map saved to {output_file}") - - # Create and write boolean map if requested - if create_boolean: - if verbose: - print( - f"Creating boolean map (threshold={boolean_threshold})" - ) - - c = hsp.HealSparseMap.make_empty(nside_coverage, nside, bool) - c[m.valid_pixels] = m[m.valid_pixels] >= boolean_threshold - - if verbose: - print(f"Writing boolean map to: {boolean_output}") - - c.write(boolean_output, clobber=True) - print(f"Boolean map saved to {boolean_output}") - - # Create plot if requested - if create_plot: - if verbose: - print(f"Creating plot of coverage map") - - try: - from shapepipe.utilities.coverage_plotter import CoveragePlotter - plotter = CoveragePlotter() - plotter.plot_coverage_map( - m, - plot_output, - region=plot_region, - vmax=boolean_threshold if create_boolean else 3, - colorbar=True, - colorbar_label="Coverage depth", - ) - print(f"Plot saved to {plot_output}") - except ImportError as e: - print(f"Warning: Could not create plot: {e}") - print("Install the cs_util package for plotting support.") - - return 0 + return hsp_map diff --git a/src/shapepipe/utilities/field_corners_extractor.py b/src/shapepipe/utilities/field_corners_extractor.py deleted file mode 100644 index 7e3273456..000000000 --- a/src/shapepipe/utilities/field_corners_extractor.py +++ /dev/null @@ -1,493 +0,0 @@ -"""FIELD_CORNERS_EXTRACTOR - -Extract per-CCD sky-footprint corner coordinates from FITS headers. - -For each CCD (image HDU) in an exposure's multi-HDU header, the four corners -of the CCD are projected from pixel bounds to the sky, producing one row per -CCD keyed on its ``-`` ID. - -Author: Mike Hudson, Martin Kilbinger - -""" - -import sys -import os -import glob -import re -from os.path import exists -from multiprocessing import Pool, cpu_count - -import numpy as np -from astropy import wcs -from astropy.io.fits import Header - -from cs_util import args as cs_args -from cs_util import logging - -# Filename suffix convention for header files: <6+ digit exposure number>.txt -_EXPNUM_RE = re.compile(r'(\d+)\.txt$') - - -def _expnum_from_path(path): - """Extract exposure number from a header filename like ``1234567.txt``.""" - match = _EXPNUM_RE.search(path) - if match is None: - raise ValueError(f"Could not extract exposure number from {path!r}") - return int(match.group(1)) - - -def _image_shape(header): - """Return the CCD image ``(nx, ny)`` pixel shape from a header. - - For fpack tile-compressed HDUs (``ZIMAGE = T``), ``NAXIS1``/``NAXIS2`` - describe the compressed *binary table* (row width in bytes, row count), - not the image, and astropy's WCS never maps the true dimensions into - ``pixel_shape``. The true image size is carried by ``ZNAXIS1``/``ZNAXIS2``, - which are preferred here; a plain image HDU falls back to - ``NAXIS1``/``NAXIS2``. - - Parameters - ---------- - header : astropy.io.fits.Header - single-HDU header - - Returns - ------- - tuple - ``(nx, ny)`` image pixel dimensions - - Raises - ------ - ValueError - if neither the ``Z*`` nor the plain ``NAXIS`` dimensions are present - """ - if header.get("ZIMAGE", False): - keys = ("ZNAXIS1", "ZNAXIS2") - else: - keys = ("NAXIS1", "NAXIS2") - - if keys[0] not in header or keys[1] not in header: - raise ValueError( - f"Header is missing image dimensions ({keys[0]}/{keys[1]})" - ) - - return int(header[keys[0]]), int(header[keys[1]]) - - -def _parse_header_to_wcs(path): - """Parse a multi-HDU header text file into ``(wcs, (nx, ny))`` per HDU. - - The primary HDU is skipped. Each remaining HDU yields its WCS together with - the CCD image pixel shape (``ZNAXIS1/2`` for compressed HDUs, else - ``NAXIS1/2``); the shape is read from the header because the WCS drops the - ``Z*`` keywords. - """ - with open(path, "r") as f: - string = f.read() - tokens = re.split(r"^(END\s+)", string, flags=re.MULTILINE) - result = [] - for i in range(2, len(tokens) - 1, 2): - header = Header.fromstring(tokens[i] + tokens[i + 1], sep="\n") - result.append((wcs.WCS(header), _image_shape(header))) - return result - - -def _ccd_corners(w, shape): - """Return (ra, dec) of the 4 corners of a single CCD. - - The polygon is built from the CCD's pixel bounds ``shape = (nx, ny)`` using - pixel edges ``(-0.5, n - 0.5)`` so the quadrilateral covers the full CCD - area rather than pixel centres. Corners are returned in a consistent - counterclockwise pixel order (bottom-left, bottom-right, top-right, - top-left); since ``pixel_to_world`` maps this order to a simple, - non-self-intersecting sky quadrilateral for a CCD-sized field, and - HealSparse ``Polygon`` is orientation-agnostic, the resulting polygon is a - valid convex footprint. - - Parameters - ---------- - w : astropy.wcs.WCS - WCS of a single CCD - shape : tuple - ``(nx, ny)`` CCD image pixel dimensions - - Returns - ------- - tuple - ``(ra_list, dec_list)`` each of length 4, in degrees - """ - nx, ny = shape - - # Pixel-edge corners, counterclockwise: BL, BR, TR, TL - px = [-0.5, nx - 0.5, nx - 0.5, -0.5] - py = [-0.5, -0.5, ny - 0.5, ny - 0.5] - - sky = w.pixel_to_world(px, py) - return list(sky.ra.deg), list(sky.dec.deg) - - -class FieldCornersExtractor(object): - """Field Corners Extractor Class. - - Extracts RA/Dec coordinates of field corners from FITS headers. - """ - - def __init__(self): - """Initialize the extractor.""" - self.params_default() - - def params_default(self): - """Set default parameters and command line options.""" - - self._params = { - "input_dir": "header", - "output_file": "exp_ra_dec.txt", - "ccd_list": None, - "resume": False, - "n_processes": 1, - "verbose": False, - } - - self._short_options = { - "input_dir": "-i", - "output_file": "-o", - "ccd_list": "-l", - "resume": "-r", - "n_processes": "-n", - } - - self._types = { - "resume": "bool", - "n_processes": "int", - "verbose": "bool", - } - - self._help_strings = { - "input_dir": "input directory containing header files; default is {}", - "output_file": "output file for per-CCD corners; default is {}", - "ccd_list": "file of valid CCD IDs (output of get_ccds_with_psf); when given, only listed CCDs are written; on --resume, CCDs new to an expanded list are added without duplicating existing rows; default is all CCDs", - "resume": "resume from existing output file; default is {}", - "n_processes": f"number of parallel processes (1=serial, 0=auto={cpu_count()}); default is {{}}", - } - - def set_params_from_command_line(self, args): - """Set Params From Command line. - - Only use when calling using python from command line. - Does not work from ipython or jupyter. - - Parameters - ---------- - args : list - command line arguments - - """ - # Read command line options - options = cs_args.parse_options( - self._params, - self._short_options, - self._types, - self._help_strings, - args=args, - ) - self._params = options - - # Save calling command - logging.log_command(args) - - def update_params(self): - """Update parameters. - - Set derived parameters based on input parameters. - """ - # Ensure input directory ends without trailing slash - if self._params["input_dir"].endswith("/"): - self._params["input_dir"] = self._params["input_dir"][:-1] - - def check_params(self): - """Check parameters for validity.""" - if not exists(self._params["input_dir"]): - raise FileNotFoundError( - f"Input directory not found: {self._params['input_dir']}" - ) - - if self._params["ccd_list"] is not None and not exists( - self._params["ccd_list"] - ): - raise FileNotFoundError( - f"CCD list file not found: {self._params['ccd_list']}" - ) - - # Set n_processes to cpu_count if 0 - if self._params["n_processes"] == 0: - self._params["n_processes"] = cpu_count() - - if self._params["n_processes"] < 0: - raise ValueError( - f"n_processes must be >= 0, got {self._params['n_processes']}" - ) - - @staticmethod - def load_ccd_list(path): - """Load CCD List. - - Read valid CCD IDs (one ``-`` per line) into a set. - - Parameters - ---------- - path : str - path to the CCD list file - - Returns - ------- - set - valid CCD IDs - - """ - with open(path) as f: - return {line.strip() for line in f if line.strip()} - - @staticmethod - def process_single_header(args): - """Process Single Header. - - Worker function to process a single header file into per-CCD corners. - Static method so it can be pickled for multiprocessing. - - Parameters - ---------- - args : tuple - ``(path, verbose, valid_ccds)`` where ``path`` is the header file - path, ``verbose`` is a bool, and ``valid_ccds`` is a set of CCD IDs - to keep (or ``None`` to keep all) - - Returns - ------- - list or None - list of ``(ccd_id, ra_list, dec_list)`` for the exposure's CCDs on - success, ``None`` on failure - - """ - path, verbose, valid_ccds = args - expnum = _expnum_from_path(path) - - try: - wcs_shapes = _parse_header_to_wcs(path) - except Exception as e: - if verbose: - print(f"Failed to process {expnum}: {e}") - return None - - rows = [] - for ccd_idx, (w, shape) in enumerate(wcs_shapes): - ccd_id = f"{expnum}-{ccd_idx}" - if valid_ccds is not None and ccd_id not in valid_ccds: - continue - try: - ra, dec = _ccd_corners(w, shape) - except Exception as e: - if verbose: - print(f"Failed to process CCD {ccd_id}: {e}") - continue - rows.append((ccd_id, ra, dec)) - - return rows - - def get_done_ccds(self): - """Get Done CCDs. - - Read the set of CCD IDs already present in the output file. Resume is - keyed on individual CCD IDs, not exposure numbers: a CCD counts as done - only if its own row is present. This keeps resume correct when a write - was interrupted mid-exposure (the missing CCDs are filled in) and when - a rerun uses an expanded ``--ccd_list`` (the newly requested CCDs are - added), and it never duplicates a row. - - Returns - ------- - set - CCD IDs already written - - """ - output_file = self._params["output_file"] - - if not exists(output_file): - return set() - - try: - ids = np.atleast_1d( - np.loadtxt(output_file, usecols=(0), dtype=str) - ) - return set(ids.tolist()) - except Exception as e: - if self._params["verbose"]: - print(f"Could not read existing output file: {e}") - return set() - - def run(self, args=None): - """Run. - - Main execution method. - - Parameters - ---------- - args : list, optional - command line arguments - - Returns - ------- - int - exit code (0 for success) - - """ - if args is None: - args = sys.argv[1:] - - # Set parameters from command line - self.set_params_from_command_line(args) - self.update_params() - self.check_params() - - # Get parameters - input_dir = self._params["input_dir"] - output_file = self._params["output_file"] - resume = self._params["resume"] - verbose = self._params["verbose"] - - # Load the set of valid CCD IDs to keep, if given - valid_ccds = None - if self._params["ccd_list"] is not None: - valid_ccds = self.load_ccd_list(self._params["ccd_list"]) - print(f"{len(valid_ccds)} valid CCDs in {self._params['ccd_list']}") - - # Find all header files - paths = glob.glob(f"{input_dir}/*.txt") - n = len(paths) - - if n == 0: - print(f"No header files found in {input_dir}/") - return 1 - - print(f"{n} header files found") - - # On resume, read the CCD IDs already written so we can skip them - # per-CCD (not per-exposure): a partially written exposure is completed - # rather than skipped, and no row is ever duplicated. - done_ccds = set() - if resume: - done_ccds = self.get_done_ccds() - print(f"{len(done_ccds)} CCDs already done") - - # Every header still needs parsing (a header may hold both done and - # not-yet-done CCDs); the per-CCD filter below decides what is written. - todo = paths - - # Get n_processes - n_processes = self._params["n_processes"] - - # Process headers - if n_processes == 1: - # Serial processing - results = self._process_serial(todo, verbose, valid_ccds) - else: - # Parallel processing - print(f"Using {n_processes} parallel processes") - results = self._process_parallel( - todo, n_processes, verbose, valid_ccds - ) - - # Flatten per-exposure CCD lists (dropping failed exposures), then drop - # CCDs already present in the output. - rows = [ - row - for res in results if res is not None - for row in res - if row[0] not in done_ccds - ] - - # Sort by exposure number, then CCD index - rows.sort(key=lambda r: (int(r[0].split("-")[0]), - int(r[0].split("-")[1]))) - - # Write results to file: " ra1 ra2 ra3 ra4 dec1 dec2 dec3 dec4" - mode = "a" if resume else "w" - with open(output_file, mode, buffering=1) as f: - for ccd_id, ra, dec in rows: - f.write(f"{ccd_id} ") - np.savetxt(f, ra, fmt="%9.5f", newline=" ") - np.savetxt(f, dec, fmt="%9.5f", newline=" ") - f.write("\n") - - n_exp_success = sum(1 for res in results if res is not None) - n_failed = len(todo) - n_exp_success - - print(f"Processed {n_exp_success} exposures, {len(rows)} new CCDs") - if n_failed > 0: - print(f"Failed to process {n_failed} exposures") - - print(f"Results written to {output_file}") - - return 0 - - def _process_serial(self, todo, verbose, valid_ccds): - """Process Serial. - - Process headers serially. - - Parameters - ---------- - todo : list - list of header file paths to process - verbose : bool - verbose output - valid_ccds : set or None - CCD IDs to keep, or ``None`` to keep all - - Returns - ------- - list - list of per-exposure CCD-row lists (or ``None`` for failures) - - """ - results = [] - n_todo = len(todo) - - for i, p in enumerate(todo): - result = self.process_single_header((p, verbose, valid_ccds)) - results.append(result) - - if verbose and i % 100 == 0: - print(f"{i:6d} / {n_todo:6d}") - - return results - - def _process_parallel(self, todo, n_processes, verbose, valid_ccds): - """Process Parallel. - - Process headers in parallel using multiprocessing. - - Parameters - ---------- - todo : list - list of header file paths to process - n_processes : int - number of parallel processes - verbose : bool - verbose output - valid_ccds : set or None - CCD IDs to keep, or ``None`` to keep all - - Returns - ------- - list - list of per-exposure CCD-row lists (or ``None`` for failures) - - """ - # Prepare arguments for worker function - args = [(p, verbose, valid_ccds) for p in todo] - - # Create pool and process - with Pool(processes=n_processes) as pool: - results = pool.map(self.process_single_header, args) - - return results diff --git a/tests/module/test_coverage.py b/tests/module/test_coverage.py index 8cbe8e743..f71050be4 100644 --- a/tests/module/test_coverage.py +++ b/tests/module/test_coverage.py @@ -1,44 +1,35 @@ -"""UNIT / PROPERTY TESTS FOR THE COVERAGE-MASK FEATURE. - -Covers the pure and lightly-fixtured logic behind the per-CCD coverage nexp -masks: exposure-number parsing, per-CCD corner extraction from plain and -fpack-compressed multi-HDU headers, ``--ccd_list`` filtering, the per-CCD -resume path, the builder's per-CCD row parsing, the accumulated -exposure-count (nexp) contract, the RA-wrap and pole guards, the power-of-two -``nside`` validation, and the ``-h``/argv handling of the console runners. The -heavy paths (production-resolution map building, plotting, multiprocessing) -are exercised end to end by the pipeline, not here. +"""UNIT / PROPERTY TESTS FOR THE COVERAGE-MASK GEOMETRY. + +Covers the pure logic the coverage nexp mask rests on, on both sides of the +``exp_footprint`` -> ``coverage_map`` handover: per-CCD image-shape resolution +from plain and fpack-compressed headers, the sky-corner projection, and then +the accumulated exposure-count (nexp) contract, the RA-wrap and pole guards, +the power-of-two ``nside`` validation and the median smoothing of a finished +map. Everything here takes arrays or headers in memory; the way a record +reaches these functions is ``tests/unit/test_exp_footprint.py``'s subject, and +the heavy paths (production-resolution map building, plotting) are exercised +end to end by the pipeline. """ -import sys -from types import SimpleNamespace - +import healsparse as hsp +import hpgeom as hpg import numpy as np import numpy.testing as npt import pytest from astropy import wcs -from hypothesis import given -from hypothesis import strategies as st +from astropy.io.fits import Header +from shapepipe.utilities.ccd_footprint import _ccd_corners, _image_shape from shapepipe.utilities.coverage_map_builder import ( - CoverageMapBuilder, + build_map, + check_nside, + median_filter, unwrap_ra, ) -from shapepipe.utilities.field_corners_extractor import ( - FieldCornersExtractor, - _ccd_corners, - _expnum_from_path, - _image_shape, - _parse_header_to_wcs, -) def _tan_wcs(crval_ra, crval_dec=0.0, nx=2080, ny=4612): - """Build a minimal 2-D TAN WCS with a populated pixel shape. - - ``nx``/``ny`` set ``pixel_shape`` so ``_header_text`` can emit matching - ``NAXIS1``/``NAXIS2`` keywords. - """ + """Build a minimal 2-D TAN WCS with a populated pixel shape.""" w = wcs.WCS(naxis=2) w.wcs.ctype = ["RA---TAN", "DEC--TAN"] w.wcs.crval = [crval_ra, crval_dec] @@ -48,112 +39,61 @@ def _tan_wcs(crval_ra, crval_dec=0.0, nx=2080, ny=4612): return w -def _header_text(wcs_list): - """Render a multi-HDU header text file: a primary HDU then one plain image - HDU per WCS, each carrying ``NAXIS1``/``NAXIS2``. - """ - blocks = ["SIMPLE = T"] - for w in wcs_list: - nx, ny = w.pixel_shape - header = w.to_header() - header["NAXIS"] = 2 - header["NAXIS1"] = nx - header["NAXIS2"] = ny - blocks.append("\n".join(str(card) for card in header.cards)) - return "".join(f"{block}\nEND \n" for block in blocks) +def _plain_header(w): + """A plain image-HDU header: ``NAXIS1``/``NAXIS2`` are the image.""" + nx, ny = w.pixel_shape + header = w.to_header() + header["NAXIS"] = 2 + header["NAXIS1"] = nx + header["NAXIS2"] = ny + return header -def _compressed_header_text(w): - """Render a one-CCD header mimicking an fpack tile-compressed HDU. +def _compressed_header(w): + """A header shaped like an fpack tile-compressed HDU. ``NAXIS1``/``NAXIS2`` describe the compressed binary table (byte width, row - count); the true image dimensions live in ``ZNAXIS1``/``ZNAXIS2``. This is - the shape of headers fetched from 'p.fits.fz' with ``head=True``. + count); the true image dimensions live in ``ZNAXIS1``/``ZNAXIS2``. """ nx, ny = w.pixel_shape - wcs_cards = "\n".join(str(card) for card in w.to_header().cards) - ccd = ( - "XTENSION= 'BINTABLE'\n" - "BITPIX = 8\n" - "NAXIS = 2\n" - "NAXIS1 = 8\n" - f"NAXIS2 = {ny}\n" - "PCOUNT = 1000000\n" - "GCOUNT = 1\n" - "TFIELDS = 1\n" - "ZIMAGE = T\n" - "ZBITPIX = -32\n" - "ZNAXIS = 2\n" - f"ZNAXIS1 = {nx}\n" - f"ZNAXIS2 = {ny}\n" - f"{wcs_cards}" - ) - return f"SIMPLE = T\nEND \n{ccd}\nEND \n" - - -# --------------------------------------------------------------------------- -# _expnum_from_path -# --------------------------------------------------------------------------- - -@pytest.mark.parametrize( - "path, expected", - [ - ("1234567.txt", 1234567), - ("/a/b/2143523.txt", 2143523), - ("headers/0000042.txt", 42), - ], -) -def test_expnum_from_path_extracts_trailing_number(path, expected): - """The trailing ``.txt`` is parsed as the exposure number.""" - assert _expnum_from_path(path) == expected - - -@pytest.mark.parametrize("path", ["no_number.txt", "1234567.fits", "abc.txt"]) -def test_expnum_from_path_raises_without_number(path): - """A filename without a trailing numeric stem raises ``ValueError``.""" - with pytest.raises(ValueError): - _expnum_from_path(path) - - -@given(st.integers(min_value=0, max_value=99999999)) -def test_expnum_from_path_roundtrips(expnum): - """Any exposure number round-trips through the filename convention.""" - assert _expnum_from_path(f"vos_headers/{expnum}.txt") == expnum + header = w.to_header() + header["NAXIS"] = 2 + header["NAXIS1"] = 8 + header["NAXIS2"] = ny + header["ZIMAGE"] = True + header["ZNAXIS"] = 2 + header["ZNAXIS1"] = nx + header["ZNAXIS2"] = ny + return header # --------------------------------------------------------------------------- # image-shape resolution (fpack ZNAXIS vs plain NAXIS) # --------------------------------------------------------------------------- -def test_image_shape_prefers_znaxis_for_compressed_header(tmp_path): +def test_image_shape_prefers_znaxis_for_compressed_header(): """A compressed HDU reports ZNAXIS dims, not the binary-table NAXIS.""" - w = _tan_wcs(100.0, 20.0, nx=2080, ny=4612) - path = tmp_path / "1234567.txt" - path.write_text(_compressed_header_text(w)) - - (parsed_w, shape), = _parse_header_to_wcs(str(path)) + header = _compressed_header(_tan_wcs(100.0, 20.0, nx=2080, ny=4612)) # WCS pixel_shape would wrongly report the compressed byte width (8). - assert parsed_w.pixel_shape == (8, 4612) + assert wcs.WCS(header).pixel_shape == (8, 4612) # _image_shape recovers the true image dimensions. - assert shape == (2080, 4612) + assert _image_shape(header) == (2080, 4612) -def test_image_shape_falls_back_to_naxis(tmp_path): - """A plain image HDU (no ZIMAGE) uses NAXIS1/NAXIS2.""" - w = _tan_wcs(100.0, 20.0, nx=2080, ny=4612) - path = tmp_path / "1234567.txt" - path.write_text(_header_text([w])) +def test_image_shape_falls_back_to_naxis(): + """A plain image HDU (no ZIMAGE) uses NAXIS1/NAXIS2. - (_, shape), = _parse_header_to_wcs(str(path)) + This is the live path for the workflow: ``headers-.npy`` carries the + decompressed header astropy hands back for a tile-compressed HDU. + """ + header = _plain_header(_tan_wcs(100.0, 20.0, nx=2080, ny=4612)) - assert shape == (2080, 4612) + assert _image_shape(header) == (2080, 4612) def test_image_shape_raises_without_dimensions(): """A header with no image dimensions raises a clear ``ValueError``.""" - from astropy.io.fits import Header - header = Header() header["CTYPE1"] = "RA---TAN" @@ -161,18 +101,14 @@ def test_image_shape_raises_without_dimensions(): _image_shape(header) -def test_compressed_header_corners_are_full_width(tmp_path): +def test_compressed_header_corners_are_full_width(): """Corners from a compressed header span the true CCD width, not 8 px.""" - w = _tan_wcs(100.0, 20.0, nx=2080, ny=4612) - path = tmp_path / "1234567.txt" - path.write_text(_compressed_header_text(w)) + header = _compressed_header(_tan_wcs(100.0, 20.0, nx=2080, ny=4612)) - (parsed_w, shape), = _parse_header_to_wcs(str(path)) - ra, dec = _ccd_corners(parsed_w, shape) + ra, dec = _ccd_corners(wcs.WCS(header), _image_shape(header)) # RA extent must reflect ~2080 px * 1e-5 deg/px * cos(dec), not 8 px. - ra_extent = max(ra) - min(ra) - assert ra_extent > 0.01 + assert max(ra) - min(ra) > 0.01 # --------------------------------------------------------------------------- @@ -195,257 +131,56 @@ def test_ccd_corners_returns_four_corners_around_centre(): npt.assert_allclose(max(dec) - min(dec), ny * 1e-5, rtol=1e-3) -def test_parse_header_to_wcs_returns_one_wcs_per_extension(tmp_path): - """One (wcs, shape) pair is returned per CCD HDU; the primary is skipped.""" - ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] - - path = tmp_path / "1234567.txt" - path.write_text(_header_text(ccd_wcs)) - - result = _parse_header_to_wcs(str(path)) - - assert len(result) == len(ccd_wcs) - assert all(shape == (2080, 4612) for _, shape in result) - - # --------------------------------------------------------------------------- -# FieldCornersExtractor.process_single_header — per-CCD rows and filtering +# nside validation # --------------------------------------------------------------------------- -def test_process_single_header_emits_one_row_per_ccd(tmp_path): - """Without a CCD list, every HDU yields a ``-`` row.""" - ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] - path = tmp_path / "1234567.txt" - path.write_text(_header_text(ccd_wcs)) - - rows = FieldCornersExtractor.process_single_header( - (str(path), False, None) - ) - - assert [r[0] for r in rows] == [ - "1234567-0", - "1234567-1", - "1234567-2", - ] - assert all(len(r[1]) == 4 and len(r[2]) == 4 for r in rows) - - -def test_process_single_header_filters_to_ccd_list(tmp_path): - """A ``valid_ccds`` set keeps only the listed CCDs of the exposure.""" - ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] - path = tmp_path / "1234567.txt" - path.write_text(_header_text(ccd_wcs)) - - rows = FieldCornersExtractor.process_single_header( - (str(path), False, {"1234567-0", "1234567-2"}) - ) - - assert [r[0] for r in rows] == ["1234567-0", "1234567-2"] - - -def test_load_ccd_list_reads_ids(tmp_path): - """The CCD list loader returns the stripped, non-blank IDs as a set.""" - path = tmp_path / "ccds.txt" - path.write_text("100-0\n100-1\n\n200-5\n") - - assert FieldCornersExtractor.load_ccd_list(str(path)) == { - "100-0", - "100-1", - "200-5", - } - - -def test_run_extract_writes_per_ccd_rows(tmp_path): - """End to end: run() writes one per-CCD row filtered by the CCD list.""" - header_dir = tmp_path / "headers" - header_dir.mkdir() - ccd_wcs = [_tan_wcs(10.0), _tan_wcs(20.0), _tan_wcs(30.0)] - (header_dir / "1234567.txt").write_text(_header_text(ccd_wcs)) - - ccd_list = tmp_path / "ccds.txt" - ccd_list.write_text("1234567-0\n1234567-2\n") - - out = tmp_path / "corners.txt" - - extractor = FieldCornersExtractor() - extractor.run( - args=[ - "-i", str(header_dir), - "-l", str(ccd_list), - "-o", str(out), - ] - ) - - lines = out.read_text().splitlines() - assert len(lines) == 2 - ids = [line.split()[0] for line in lines] - assert ids == ["1234567-0", "1234567-2"] - # Each row: 1 ID + 4 RA + 4 Dec = 9 columns. - assert all(len(line.split()) == 9 for line in lines) - - -# --------------------------------------------------------------------------- -# FieldCornersExtractor resume path (per-CCD done-set) -# --------------------------------------------------------------------------- - -def _write_headers(header_dir, expnums, n_ccd=3): - """Write one plain multi-HDU header per exposure into ``header_dir``.""" - header_dir.mkdir(exist_ok=True) - for j, expnum in enumerate(expnums): - ccd_wcs = [_tan_wcs(10.0 + j + i) for i in range(n_ccd)] - (header_dir / f"{expnum}.txt").write_text(_header_text(ccd_wcs)) - - -def test_get_done_ccds_reads_present_ids(tmp_path): - """The done-set is the exact set of CCD IDs already in the output.""" - out = tmp_path / "corners.txt" - out.write_text( - "1234567-0 10 10 10 10 20 20 20 20\n" - "1234567-2 10 10 10 10 20 20 20 20\n" - ) - - extractor = FieldCornersExtractor() - extractor._params["output_file"] = str(out) - - assert extractor.get_done_ccds() == {"1234567-0", "1234567-2"} - - -def test_resume_adds_new_exposure_without_duplicating(tmp_path): - """Resume appends a new exposure and leaves existing rows untouched.""" - header_dir = tmp_path / "headers" - _write_headers(header_dir, [1000001, 1000002]) - - out = tmp_path / "corners.txt" - # Pre-populate with exposure 1's three CCD rows. - pre = FieldCornersExtractor().process_single_header( - (str(header_dir / "1000001.txt"), False, None) - ) - with open(out, "w") as f: - for ccd_id, ra, dec in pre: - f.write(f"{ccd_id} " + " ".join(f"{v:.5f}" for v in ra + dec) + "\n") - - FieldCornersExtractor().run( - args=["-i", str(header_dir), "-o", str(out), "-r"] - ) - - ids = [line.split()[0] for line in out.read_text().splitlines()] - # No duplicate exposure-1 rows; exposure 2's three CCDs added. - assert ids.count("1000001-0") == 1 - assert sorted(ids) == [ - "1000001-0", "1000001-1", "1000001-2", - "1000002-0", "1000002-1", "1000002-2", - ] - - -def test_resume_completes_partial_exposure(tmp_path): - """An exposure interrupted mid-write is completed, not skipped.""" - header_dir = tmp_path / "headers" - _write_headers(header_dir, [1000001]) - - out = tmp_path / "corners.txt" - # Simulate an interrupt: only the first of exposure 1's CCDs was written. - pre = FieldCornersExtractor().process_single_header( - (str(header_dir / "1000001.txt"), False, None) - ) - ccd_id, ra, dec = pre[0] - with open(out, "w") as f: - f.write(f"{ccd_id} " + " ".join(f"{v:.5f}" for v in ra + dec) + "\n") - - FieldCornersExtractor().run( - args=["-i", str(header_dir), "-o", str(out), "-r"] - ) - - ids = [line.split()[0] for line in out.read_text().splitlines()] - # The missing CCDs are filled in; the present one is not duplicated. - assert sorted(ids) == ["1000001-0", "1000001-1", "1000001-2"] - assert ids.count("1000001-0") == 1 - - -# --------------------------------------------------------------------------- -# CoverageMapBuilder.check_params (nside validation) -# --------------------------------------------------------------------------- - -def test_check_params_accepts_power_of_two_nside(tmp_path): +def test_check_nside_accepts_powers_of_two(): """Powers of two for both nside values pass validation.""" - infile = tmp_path / "corners.txt" - infile.write_text("100-0 0.0 1.0 1.0 0.0 0.0 0.0 1.0 1.0\n") - - builder = CoverageMapBuilder() - builder._params["input_file"] = str(infile) - builder._params["nside_coverage"] = 32 - builder._params["nside"] = 2048 - - builder.check_params() + check_nside(32, 2048) @pytest.mark.parametrize( - "nside_coverage, nside", [(33, 2048), (32, 100), (32, 3000)] + "nside_coverage, nside", [(33, 2048), (32, 100), (32, 3000), (0, 2048)] ) -def test_check_params_rejects_non_power_of_two_nside( - tmp_path, nside_coverage, nside -): +def test_check_nside_rejects_non_power_of_two(nside_coverage, nside): """A non-power-of-two nside raises ``ValueError``.""" - infile = tmp_path / "corners.txt" - infile.write_text("100-0 0.0 1.0 1.0 0.0 0.0 0.0 1.0 1.0\n") - - builder = CoverageMapBuilder() - builder._params["input_file"] = str(infile) - builder._params["nside_coverage"] = nside_coverage - builder._params["nside"] = nside - with pytest.raises(ValueError): - builder.check_params() + check_nside(nside_coverage, nside) -def test_check_params_missing_input_file_raises(tmp_path): - """A missing input file raises ``FileNotFoundError``.""" - builder = CoverageMapBuilder() - builder._params["input_file"] = str(tmp_path / "does_not_exist.txt") - - with pytest.raises(FileNotFoundError): - builder.check_params() +def test_build_map_validates_nside_before_stamping(): + """``build_map`` refuses a bad nside rather than failing deep in healsparse.""" + with pytest.raises(ValueError, match="nside"): + build_map(["100-0"], [0.0, 0.1, 0.1, 0.0], [0.0, 0.0, 0.1, 0.1], + 32, 3000) # --------------------------------------------------------------------------- -# CoverageMapBuilder — parsing, nexp contract, RA-wrap and pole guards +# build_map — nexp contract, RA-wrap and pole guards # --------------------------------------------------------------------------- -def _read_map(path): - import healsparse as hsp - - return hsp.HealSparseMap.read(str(path)) - - -def test_build_map_single_row(tmp_path): - """A one-row corners file exercises the atleast_1d/2d parsing guards.""" - infile = tmp_path / "corners.txt" - infile.write_text("100-0 9.9 10.1 10.1 9.9 0.0 0.0 0.2 0.2\n") - out = tmp_path / "cov.hsp" - - CoverageMapBuilder().run( - args=["-i", str(infile), "-o", str(out), "-c", "32", "-n", "1024"] +def test_build_map_single_footprint(): + """One footprint given as flat corner lists exercises the shape guards.""" + m = build_map( + ["100-0"], [9.9, 10.1, 10.1, 9.9], [0.0, 0.0, 0.2, 0.2], 32, 1024 ) - m = _read_map(out) assert 0 < len(m.valid_pixels) < 200 assert m[m.valid_pixels].max() == 1 -def test_build_map_nexp_counts_overlapping_exposures(tmp_path): +def test_build_map_nexp_counts_overlapping_exposures(): """Two overlapping CCDs from different exposures give value 2 in overlap.""" # Two 0.4x0.4 deg CCDs offset by 0.2 deg in RA -> a central overlap strip. - infile = tmp_path / "corners.txt" - infile.write_text( - "100-0 9.8 10.2 10.2 9.8 19.8 19.8 20.2 20.2\n" - "200-0 10.0 10.4 10.4 10.0 19.8 19.8 20.2 20.2\n" + m = build_map( + ["100-0", "200-0"], + [[9.8, 10.2, 10.2, 9.8], [10.0, 10.4, 10.4, 10.0]], + [[19.8, 19.8, 20.2, 20.2], [19.8, 19.8, 20.2, 20.2]], + 32, + 1024, ) - out = tmp_path / "cov.hsp" - CoverageMapBuilder().run( - args=["-i", str(infile), "-o", str(out), "-c", "32", "-n", "1024"] - ) - - m = _read_map(out) values = m[m.valid_pixels] # The overlap is covered by both exposures (value 2); the union edges by # one (value 1). Both must be present; nothing exceeds 2. @@ -469,30 +204,19 @@ def test_unwrap_ra_leaves_normal_polygon_unchanged(): ) -def test_build_map_invariant_under_ra_shift(tmp_path): +def test_build_map_invariant_under_ra_shift(): """A seam CCD's footprint matches an identical CCD shifted +10 deg in RA. - Rotating both the seam CCD (via +360/unwrap) and a reference CCD onto the - same RA and comparing pixel counts fails if the seam polygon were filling + Comparing the seam polygon's pixel count against a reference polygon of the + same size at the same declination fails if the seam polygon were filling the ~360 deg complement. This is the real RA-wrap regression guard. """ - # Seam CCD straddling RA=0, and the same CCD translated to RA~10. - seam = tmp_path / "seam.txt" - seam.write_text("100-0 359.9 0.1 0.1 359.9 20.0 20.0 20.2 20.2\n") - ref = tmp_path / "ref.txt" - ref.write_text("200-0 9.9 10.1 10.1 9.9 20.0 20.0 20.2 20.2\n") - - m_seam = tmp_path / "seam.hsp" - m_ref = tmp_path / "ref.hsp" - CoverageMapBuilder().run( - args=["-i", str(seam), "-o", str(m_seam), "-c", "32", "-n", "1024"] - ) - CoverageMapBuilder().run( - args=["-i", str(ref), "-o", str(m_ref), "-c", "32", "-n", "1024"] - ) + dec = [20.0, 20.0, 20.2, 20.2] + m_seam = build_map(["100-0"], [359.9, 0.1, 0.1, 359.9], dec, 32, 1024) + m_ref = build_map(["200-0"], [9.9, 10.1, 10.1, 9.9], dec, 32, 1024) - n_seam = len(_read_map(m_seam).valid_pixels) - n_ref = len(_read_map(m_ref).valid_pixels) + n_seam = len(m_seam.valid_pixels) + n_ref = len(m_ref.valid_pixels) # Same-size footprints at the same declination: pixel counts agree to # within a few boundary pixels, and are nowhere near a hemisphere. @@ -500,17 +224,14 @@ def test_build_map_invariant_under_ra_shift(tmp_path): assert 0 < n_seam < 200 -def test_build_map_pole_guard_skips_polygon(tmp_path, capsys): +def test_build_map_pole_guard_skips_polygon(capsys): """A polygon with |dec| near 90 deg is skipped with a warning.""" - infile = tmp_path / "corners.txt" - infile.write_text( - "100-0 10.0 10.2 10.2 10.0 89.5 89.5 89.7 89.7\n" - "200-0 10.0 10.2 10.2 10.0 20.0 20.0 20.2 20.2\n" - ) - out = tmp_path / "cov.hsp" - - CoverageMapBuilder().run( - args=["-i", str(infile), "-o", str(out), "-c", "32", "-n", "1024"] + build_map( + ["100-0", "200-0"], + [[10.0, 10.2, 10.2, 10.0], [10.0, 10.2, 10.2, 10.0]], + [[89.5, 89.5, 89.7, 89.7], [20.0, 20.0, 20.2, 20.2]], + 32, + 1024, ) captured = capsys.readouterr() @@ -519,19 +240,17 @@ def test_build_map_pole_guard_skips_polygon(tmp_path, capsys): # --------------------------------------------------------------------------- -# console-runner argv handling (regression guard for the -h entry points) +# median smoothing (offline; the coverage_map rule writes the raw nexp map) # --------------------------------------------------------------------------- -def test_run_help_flag_exits_cleanly(monkeypatch): - """``run()`` with no args reads ``sys.argv[1:]`` so ``-h`` exits 0. - - Guards the regression where the runners parsed the full ``sys.argv`` - (including ``argv[0]``), which made ``-h`` collide with the integer - ``-c`` option and exit non-zero. - """ - monkeypatch.setattr(sys, "argv", ["extract_field_corners", "-h"]) +def test_median_filter_fills_a_lone_hole(): + """A single low pixel inside a covered disc is pulled up to its neighbours.""" + nside = 1024 + m = hsp.HealSparseMap.make_empty(32, nside, np.uint16) + m[hpg.query_circle(nside, 10.0, 20.0, 0.1)] = 2 - with pytest.raises(SystemExit) as excinfo: - FieldCornersExtractor().run() + centre = hpg.angle_to_pixel(nside, 10.0, 20.0) + m[[centre]] = 1 + assert m[centre] == 1 - assert excinfo.value.code == 0 + assert median_filter(m)[centre] == 2 diff --git a/workflow/README.md b/workflow/README.md index 6299fc2d7..1f3eee4c1 100644 --- a/workflow/README.md +++ b/workflow/README.md @@ -266,11 +266,10 @@ profiles/nibi/config.yaml SLURM executor; apptainer SDM; per-user jobs cap; kee counting, per sky pixel, the exposures with a valid PSF there. That job is **campaign-cumulative**: its declared inputs are the in-scope footprints, but the script reads *every* record on the products root, reclaimed exposures - included, so appending tiles grows the map instead of replacing it. `nside` - defaults to the production 128/131072 pair, ~0.1"/pixel, chosen to align - pixel-wise with the UNIONS bit masks — not the `CoverageMapBuilder` class - defaults (32/2048), which would produce a plausible-looking map that does not - align. Plotting stays out of the DAG: run `plot_coverage_map -i + included, so appending tiles grows the map instead of replacing it. `nside` is + set in `config.yaml` to the production 128/131072 pair, ~0.1"/pixel, chosen to + align pixel-wise with the UNIONS bit masks; nothing defaults to it, and a + coarser map would look plausible and not align. Plotting stays out of the DAG: run `plot_coverage_map -i /coverage/coverage.hsp ...` by hand, with the sky windows under `coverage.plot` in `config.yaml`. sp_validation consumes the map in `notebooks/demo_apply_hsp_masks.py`. diff --git a/workflow/config.yaml b/workflow/config.yaml index 2c5234a78..32e3ffedb 100644 --- a/workflow/config.yaml +++ b/workflow/config.yaml @@ -240,11 +240,13 @@ ngmix_chunks: 8 # FILE_NOT_FOUND). That makes `validation_psf-*.fits` in `persist_exp:` above a # PRECONDITION: empty that list and no valid-PSF record exists to build from. # -# nside_coverage / nside: 128 / 131072 is the PRODUCTION pair, and it is not the -# CoverageMapBuilder default (32 / 2048, meant for lighter-weight offline use). -# nside=131072 is ~0.1"/pixel, CHOSEN TO MATCH THE UNIONS BIT-MASK RESOLUTION so -# that coverage and mask align pixel-wise. Inheriting the class defaults yields a -# mask that does not align, and the consumer would not notice. +# nside_coverage / nside: 128 / 131072 is the PRODUCTION pair, and NOTHING +# defaults to it — build_map takes both as required arguments, and this block is +# now the only place the pair is written down (the v1.x shell script that carried +# it is retired). nside=131072 is ~0.1"/pixel, CHOSEN TO MATCH THE UNIONS +# BIT-MASK RESOLUTION so that coverage and mask align pixel-wise. A coarser map +# looks entirely reasonable and does not align, and the consumer would not +# notice. # # Consumer: sp_validation applies these healsparse structural masks in # notebooks/demo_apply_hsp_masks.py (it reads the coverage .hsp and writes the diff --git a/workflow/scripts/coverage_map.py b/workflow/scripts/coverage_map.py index 0a2e968bd..88039a434 100644 --- a/workflow/scripts/coverage_map.py +++ b/workflow/scripts/coverage_map.py @@ -19,18 +19,16 @@ where a reader of the DAG will meet it. THE STAMPING IS NOT HERE. It is ``shapepipe.utilities.coverage_map_builder. -build_map``, shared with the ``build_coverage_map`` CLI, so the RA-seam guard, -the pole guard and the polygon accumulation have exactly one implementation. This -script is the JSON-to-arrays half, and the records are raw sky: unwrapping across -RA=0 happens once, inside build_map, at the moment a polygon is stamped. - -NSIDE IS NOT DEFAULTED HERE. Both values are required arguments, carried from -`coverage:` in config.yaml, because the production pair (128 / 131072) is NOT the -CoverageMapBuilder default (32 / 2048) and the difference is invisible in the -output: nside=131072 is ~0.1"/pixel, chosen to match the UNIONS bit-mask -resolution so coverage and mask align pixel-wise. A map built at the class -default would look entirely reasonable and would not align, and the consumer -would not notice. +build_map``, which is where the RA-seam guard, the pole guard and the polygon +accumulation live. This script is the JSON-to-arrays half, and the records are +raw sky: unwrapping across RA=0 happens once, inside build_map, at the moment a +polygon is stamped. + +NSIDE IS NOT DEFAULTED ANYWHERE. Both values are required arguments, carried +from `coverage:` in config.yaml, and the difference they make is invisible in +the output: nside=131072 is ~0.1"/pixel, chosen to match the UNIONS bit-mask +resolution so coverage and mask align pixel-wise. A map built coarser would look +entirely reasonable and would not align, and the consumer would not notice. """ import argparse diff --git a/workflow/scripts/exp_footprint.py b/workflow/scripts/exp_footprint.py index 54aa90198..d4573620b 100644 --- a/workflow/scripts/exp_footprint.py +++ b/workflow/scripts/exp_footprint.py @@ -69,7 +69,7 @@ import numpy as np from astropy.io.fits import Header -from shapepipe.utilities.field_corners_extractor import ( +from shapepipe.utilities.ccd_footprint import ( _ccd_corners, _image_shape, ) From c116ea96c2cc75f6c2cce2651d86c92dfe2b5e9a Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:24:11 +0200 Subject: [PATCH 20/36] refactor: delete the summary scrape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit summary.py reconstructed a campaign's state by walking a finished run's patch directories: a five-part path recipe, newest-run_sp_* resolution, n_CCD = 40 hardcoded, bit-named outputs (missing_job__all.txt) and an eight-code error-signature whitelist driving a resubmission loop. summary_params_pre_v2.py was its parameter table, and every line of it is a per-patch branch, a -j bit key or a canfar-era run-dir name — `n_sh = 1 if patch in ("P2","P5","P8","P9") else 8`, a run root under $HOME/cosmostat/v2/pre_v2, a hand-appended workaround. The workflow does not scrape. Each rule records its own completeness as it goes (completeness.py's per-stage floors and reason scrape, written to manifests/.json) and run_report.py tallies those records, so the state of a campaign is read rather than inferred. The patch is not a concept either: scope is a tile list, and summary_tiles.py's hardcoded `n_patch = 8` — bumped 7->8 in 2025-01 and never bumped for P9 — is what that assumption costs. Also deleted: summary_run.py and its bin/ shim, summary_tiles.py, and summary_run.ipynb, which has been dying at cell 3 since summary_params_pre_v2 moved into the package and broke its `%run` path. The summary_run entry point goes in the same commit as the module (test_entrypoints.py), and CLAUDE.md's entry-point list is updated to what actually ships. The last importer was ccd_psf_handler.py, retired one commit ago; nothing in src/, workflow/ or tests/ imports summary now. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- CLAUDE.md | 2 +- bin/summary_run.py | 3 - pyproject.toml | 1 - scripts/jupyter/summary_run.ipynb | 399 --------- scripts/python/summary_tiles.py | 183 ---- src/shapepipe/summary_run.py | 102 --- src/shapepipe/utilities/__init__.py | 2 +- src/shapepipe/utilities/summary.py | 832 ------------------ .../utilities/summary_params_pre_v2.py | 276 ------ tests/unit/test_exp_footprint.py | 2 +- 10 files changed, 3 insertions(+), 1799 deletions(-) delete mode 100755 bin/summary_run.py delete mode 100644 scripts/jupyter/summary_run.ipynb delete mode 100755 scripts/python/summary_tiles.py delete mode 100755 src/shapepipe/summary_run.py delete mode 100755 src/shapepipe/utilities/summary.py delete mode 100644 src/shapepipe/utilities/summary_params_pre_v2.py diff --git a/CLAUDE.md b/CLAUDE.md index 8ee90c2cc..eebbc386e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,7 +81,7 @@ Full detail: `docs/source/installation.md` and `docs/source/container.md`. - `src/shapepipe/` — the package (src-layout). `modules/` holds the pipeline modules and their `*_runner.py` wrappers; `pipeline/` is execution and file I/O; `utilities/`; `canfar/` is CANFAR/cluster job orchestration. Console entry - points (`shapepipe_run`, `summary_run`, `canfar_*`) are defined under + points (`shapepipe_run`, `canfar_*`, `plot_coverage_map`) are defined under `[project.scripts]`. - `tests/` — the whole test suite, one discovery root: `module/` (per-module unit/property/integration tests), `unit/` (structural), `science/` (fast diff --git a/bin/summary_run.py b/bin/summary_run.py deleted file mode 100755 index a59689e41..000000000 --- a/bin/summary_run.py +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python -from shapepipe.summary_run import main -main() diff --git a/pyproject.toml b/pyproject.toml index b400d618f..c90eb908d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,7 +84,6 @@ dev = ["shapepipe[doc,jupyter,lint,plot,release,test,fitsio]"] [project.scripts] shapepipe_run = "shapepipe.shapepipe_run:main" -summary_run = "shapepipe.summary_run:main" canfar_submit_job = "shapepipe.canfar_run:run_job" canfar_monitor = "shapepipe.canfar_run:run_log" canfar_monitor_log = "shapepipe.canfar_run:run_monitor_log" diff --git a/scripts/jupyter/summary_run.ipynb b/scripts/jupyter/summary_run.ipynb deleted file mode 100644 index 44f2f2545..000000000 --- a/scripts/jupyter/summary_run.ipynb +++ /dev/null @@ -1,399 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "130112a4-f2ca-4d26-b884-d8b054676f9c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 3\n", - "%reload_ext autoreload" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "196beca5-10a1-4cf5-9462-be145167cc70", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "ename": "ModuleNotFoundError", - "evalue": "No module named 'shapepipe'", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[1], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mshapepipe\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mshapepipe\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mutilities\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01msummary\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n", - "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'shapepipe'" - ] - } - ], - "source": [ - "import shapepipe\n", - "from shapepipe.utilities.summary import *" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "ca63c72d-212c-463e-a792-71efbac0b908", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Setting\n", - "patch = \"P7\"\n", - "\n", - "verbose = False" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "dcb5604c-d61f-4705-8295-63875455cadb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Load parameters\n", - "%run ~/shapepipe/scripts/python/summary_params_pre_v2" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "e69b7dab-1fea-4fcc-a8d9-0720e1d628c3", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Checking main directory = /arc/home/kilbinger/cosmostat/v2/pre_v2/psfex/P7\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Set job info for patch P7\n" - ] - } - ], - "source": [ - "jobs, list_tile_IDs = set_jobs_v2_pre_v2(patch, verbose)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "2c3a9dde-cf88-493f-926e-7ae7e8e10916", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Initialize runtime dicionary.\n", - "par_runtime = init_par_runtime(list_tile_IDs)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "1e9c1487-3cec-4394-9fcf-c12e92a0f984", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# No effect in notebook\n", - "#print_par_runtime(par_runtime, verbose=verbose)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "b7c63a22-ead1-4d6a-b081-a74ade515439", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "module expected found miss_expl missing uniq_miss fr_found\n", - "====================================================================================================\n" - ] - } - ], - "source": [ - "# Start program\n", - "job_data.print_stats_header()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "4720ae18-0633-4646-b392-b1b24e0294c3", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - " (Job 1)\n", - "get_images_runner_run_1 462 462 0 0 0.0 100.0%\n", - "find_exposures_runner 231 231 0 0 0.0 100.0%\n", - "get_images_runner_run_2 537 0 0 537 179.0 0.0%\n" - ] - } - ], - "source": [ - "for key in \"1\":\n", - " jobs[key].print_intro()\n", - " jobs[key].check_numbers(par_runtime=par_runtime, indices=[0, 1])\n", - "\n", - " all_exposures = get_all_exposures(jobs[key]._paths_in_dir[1], verbose=verbose)\n", - " par_runtime[\"n_exposures\"] = len(all_exposures)\n", - " par_runtime[\"list_exposures\"] = all_exposures\n", - "\n", - " jobs[key].check_numbers(par_runtime, indices=[2])" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f149f404-64e7-4d92-8f54-f300ed620130", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Update runtime dictionary with numbers of exposures\n", - "par_runtime = update_par_runtime_after_find_exp(par_runtime, all_exposures)" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "78a9065f-8983-41cf-a34c-21892fc52dd2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Get all keys after \"1\"\n", - "keys = sorted(jobs.keys(), key=int)\n", - "_ = keys.pop(0)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "79e39954-1155-4ca3-b0b2-64bc5670db53", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - " (Job 2)\n", - "uncompress_fits_runner 1268 1268 0 0 0.0 100.0%\n", - "merge_headers_runner 0 0 0 0 0.0 100.0%\n", - "split_exp_runner 137940 137940 0 0 0.0 100.0%\n", - " (Job 4)\n", - "mask_runner 1268 1268 0 0 0.0 100.0%\n", - " (Job 8)\n", - "mask_runner 45600 45600 0 0 0.0 100.0%\n", - " (Job 16)\n", - "sextractor_runner 2536 2536 0 0 0.0 100.0%\n", - " (Job 32)\n", - "sextractor_runner 91200 91200 0 0 0.0 100.0%\n", - "setools_runner 91200 91032 0 168 84.0 99.8%\n", - "psfex_runner 91200 91032 0 168 84.0 99.8%\n", - " (Job 64)\n", - "psfex_interp_runner 1268 1268 0 0 0.0 100.0%\n", - "vignetmaker_runner_run_1 1268 1268 0 0 0.0 100.0%\n", - "spread_model_runner 1268 1268 0 0 0.0 100.0%\n", - "vignetmaker_runner_run_2 5072 5072 0 0 0.0 100.0%\n", - " (Job 128)\n", - "ngmix_runner 1268 1225 0 43 43.0 96.6%\n", - "ngmix_runner 1268 1216 0 52 52.0 95.9%\n", - "ngmix_runner 1268 1216 0 52 52.0 95.9%\n", - "ngmix_runner 1268 1217 0 51 51.0 96.0%\n", - "ngmix_runner 1268 1228 0 40 40.0 96.8%\n", - "ngmix_runner 1268 1216 0 52 52.0 95.9%\n", - "ngmix_runner 1268 1216 0 52 52.0 95.9%\n", - "ngmix_runner 1268 1216 0 52 52.0 95.9%\n", - " (Job 256)\n", - "merge_sep_cats_runner 1268 0 0 1268 1268.0 0.0%\n", - "make_cat_runner 1268 0 0 1268 1268.0 0.0%\n", - " (Job 1024)\n", - "psfex_interp_runner 45600 41132 0 4468 4468.0 90.2%\n" - ] - } - ], - "source": [ - "for key in keys:\n", - " jobs[key].print_intro()\n", - " jobs[key].check_numbers(par_runtime=par_runtime)" - ] - }, - { - "cell_type": "code", - "execution_count": 65, - "id": "b3d51a05-ecca-420b-b8b3-1fb2b1ec9fe3", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - " (Job 128)\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1265 0 3 3.0 99.8%\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1266 0 2 2.0 99.8%\n", - "ngmix_runner 1268 1268 0 0 0.0 100.0%\n", - "ngmix_runner 1268 1266 0 2 2.0 99.8%\n" - ] - } - ], - "source": [ - "## Update some runs\n", - "for key in [\"128\"]:\n", - " jobs[key].print_intro()\n", - " jobs[key].check_numbers(par_runtime=par_runtime)" - ] - }, - { - "cell_type": "code", - "execution_count": 64, - "id": "67b50a61-e3cc-4559-941d-f39c6a200294", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - " (Job 128)\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1265 0 3 3.0 99.8%\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1267 0 1 1.0 99.9%\n", - "ngmix_runner 1268 1266 0 2 2.0 99.8%\n", - "ngmix_runner 1268 1268 0 0 0.0 100.0%\n", - "ngmix_runner 1268 1266 0 2 2.0 99.8%\n" - ] - } - ], - "source": [ - "for key in [\"128\"]:\n", - " jobs[key].print_intro()\n", - " \n", - " \n", - " jobs[key].check_numbers(par_runtime=par_runtime)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "affa8293-daf9-4d2b-9215-fe19f8e2c1e2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "session = Session()" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "id": "269496d1-cd89-4d13-a5e4-41b897669e22", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "ids = [session[\"id\"] for session in session.fetch(kind=\"headless\")]" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "80af8dff-98c7-4db4-8bcc-06936e1875cf", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "ename": "RuntimeError", - "evalue": "This event loop is already running", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m/tmp/ipykernel_69/559116804.py\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0msession\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdestroy\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mids\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m0\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m~/.local/lib/python3.7/site-packages/skaha/session.py\u001b[0m in \u001b[0;36mdestroy\u001b[0;34m(self, id)\u001b[0m\n\u001b[1;32m 264\u001b[0m \u001b[0marguments\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mappend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0;34m\"url\"\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mserver\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0;34m\"/\"\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0mvalue\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 265\u001b[0m \u001b[0mloop\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_event_loop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 266\u001b[0;31m \u001b[0mresults\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mloop\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_until_complete\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mscale\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msession\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdelete\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0marguments\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 267\u001b[0m \u001b[0mresponses\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mDict\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mstr\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mbool\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;34m{\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 268\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mindex\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0midentity\u001b[0m \u001b[0;32min\u001b[0m \u001b[0menumerate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mid\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/conda/lib/python3.7/asyncio/base_events.py\u001b[0m in \u001b[0;36mrun_until_complete\u001b[0;34m(self, future)\u001b[0m\n\u001b[1;32m 561\u001b[0m \"\"\"\n\u001b[1;32m 562\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_check_closed\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 563\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_check_runnung\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 564\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 565\u001b[0m \u001b[0mnew_task\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0mfutures\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0misfuture\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfuture\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/opt/conda/lib/python3.7/asyncio/base_events.py\u001b[0m in \u001b[0;36m_check_runnung\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 521\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0m_check_runnung\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 522\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mis_running\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 523\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'This event loop is already running'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 524\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mevents\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_get_running_loop\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mis\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32mNone\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 525\u001b[0m raise RuntimeError(\n", - "\u001b[0;31mRuntimeError\u001b[0m: This event loop is already running" - ] - } - ], - "source": [ - "session.destroy(ids[0])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "66a3ed14-8aaf-4028-b933-10ecb7376d68", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/scripts/python/summary_tiles.py b/scripts/python/summary_tiles.py deleted file mode 100755 index 9addbb017..000000000 --- a/scripts/python/summary_tiles.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python - -import sys -import os -import re -import numpy as np - -from tqdm import tqdm - - -def get_input_IDs_patch(tiles, patch): - - # Read in files from summary output with missing CCDs. - # Create and update CCD name set. - fname = f"{patch}/tile_numbers.txt" - with open(fname) as f: - lines = f.readlines() - for line in lines: - ID = line.rstrip("\n") - tiles.append( - { - "ID": ID, - "patch": patch, - } - ) - - return tiles - - -def check_zero_weight(tiles, patches): - - for patch in tqdm(patches, desc="check zero weight"): - - fname = f"{patch}/summary/special_job_16_sextractor_runner_5.txt" - - if os.path.exists(fname): - - with open(fname) as f: - lines = f.readlines() - for line in lines: - m = re.search("process-(\d{3})-(\d{3})\.log", line) - if m: - ID = f"{m[1]}.{m[2]}" - my_tile = next((tile for tile in tiles if tile.get("ID") == ID), None) - my_tile["zero_weight"] = 1 - - return tiles - - -def check_exceptions(tiles, patches): - - for patch in tqdm(patches, desc="check processing exceptions"): - - fname = f"{patch}/exceptions.txt" - - if os.path.exists(fname): - - with open(fname) as f: - lines = f.readlines() - for line in lines: - m = re.search("(\d{3})\.(\d{3})\s+\t+(.*)", line) - if m: - ID = f"{m[1]}.{m[2]}" - my_tile = next((tile for tile in tiles if tile.get("ID") == ID), None) - my_tile[m[3]] = 1 - - return tiles - - -def get_final(tiles, patches): - - n_final = {} - - for patch in tqdm(patches, desc="check processing exceptions"): - - n_final[patch] = 0 - - fname = f"{patch}/n_tiles_final.txt" - if os.path.exists(fname): - with open(fname) as f: - lines = f.readlines() - for line in lines: - num = line.rstrip("\n") - n_final[patch] = int(num) - - n_final["all"] = sum(n_final.values()) - - return n_final - - -def fill_True(tiles, keys): - - - for tile in tiles: - for key in keys: - tile[key] = 0 - - return tiles - - -def summary(patches, keys): - - tiles = [] - - n_final = get_final(tiles, patches) - - # Loop over patches - for patch in tqdm(patches, desc="read input tile lists"): - tiles = get_input_IDs_patch(tiles, patch) - - tiles = fill_True(tiles, keys) - - tiles = check_zero_weight(tiles, patches) - - tiles = check_exceptions(tiles, patches) - - return tiles, n_final - -def print_tile(tile, f_out, keys): - - for key in keys: - print(f"{tile[key]}", file=f_out, end=" ") - print(file=f_out) - - -def output_tiles(tiles, path, keys): - - with open(path, "w") as f_out: - - for tile in tiles: - print_tile(tile, f_out, keys) - - -def print_summary(tiles, patches, n_final, keys, keys_sum, nform): - - for key in keys_sum: - print(key.rjust(nform, " "), end="") - print("tot_miss".rjust(nform, " ")) - - for patch in patches: - tiles_patch = [entry for entry in tiles if entry.get("patch") == patch] - print_summary_patch(tiles_patch, patch, n_final[patch], keys, nform) - print_summary_patch(tiles, "all", n_final["all"], keys, nform) - -def print_summary_patch(tiles, patch, n_final, keys, nform): - - n_tiles = len(tiles) - print(patch.rjust(nform, " "), end="") - print(str(n_tiles).rjust(nform, " "), end="") - print(str(n_final).rjust(nform, " "), end="") - n_all = 0 - for key in keys: - n = sum(v[key] for v in tiles) - print(str(n).rjust(nform, " "), end="") - n_all += n - print(str(n_all).rjust(nform, " "), f"{n_all / n_tiles:10.2%}", end="") - print() - - -def main(argv): - - n_patch = 8 - patches = [f'P{x}' for x in np.arange(n_patch) + 1] - - keys = ["zero_weight", "aborted", "large_num_det"] - - nform = 15 - - tiles, n_final = summary(patches, keys) - - output_path = "tile_summary.txt" - - keys_ext = ["ID", "patch"] + keys - output_tiles(tiles, output_path, keys_ext) - - keys_sum = ["patch", "n_tiles", "n_final"] + keys - print_summary(tiles, patches, n_final, keys, keys_sum, nform) - - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/src/shapepipe/summary_run.py b/src/shapepipe/summary_run.py deleted file mode 100755 index 39d60ce08..000000000 --- a/src/shapepipe/summary_run.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python - -import argparse -import sys -import os - -from shapepipe.utilities import summary - -from shapepipe.utilities import summary_params_pre_v2 as summary_params - - -def run(patch, job_exclusive=None, verbose=False): - - jobs, list_tile_IDs_dot = summary_params.set_jobs_v2_pre_v2(patch, verbose) - - list_tile_IDs = summary.job_data.replace_dot_dash(list_tile_IDs_dot) - - # Numbers updated at runtime - par_runtime = summary.init_par_runtime(list_tile_IDs) - - summary.job_data.print_stats_header() - - exp_IDs_path = "exp_numbers.txt" - if os.path.exists(exp_IDs_path): - # Read exposure ID list if file exists - all_exposures = summary.get_IDs_from_file(exp_IDs_path) - par_runtime = summary.update_par_runtime_after_find_exp( - par_runtime, all_exposures - ) - - if ( - not os.path.exists(exp_IDs_path) - or not job_exclusive - or int(job_exclusive) & 1 - ): - # Run job 1 if exposure ID list file does not exist or - # job_exclusive is 1 or not set - key = "1" - jobs[key].print_intro() - jobs[key].check_numbers(par_runtime=par_runtime, indices=[0, 1]) - - all_exposures = summary.get_all_exposures( - jobs[key]._paths_in_dir[1], verbose=True - ) - par_runtime = summary.update_par_runtime_after_find_exp( - par_runtime, all_exposures - ) - - jobs[key].write_IDs_to_file("exp_numbers.txt", all_exposures) - - jobs[key].check_numbers(par_runtime, indices=[2]) - - summary.print_par_runtime(par_runtime, verbose=verbose) - - # Get all keys after "1" - keys = sorted(jobs.keys(), key=int) - _ = keys.pop(0) - - for key in keys: - if job_exclusive and not int(key) & int(job_exclusive): - continue - jobs[key].print_intro() - jobs[key].check_numbers(par_runtime=par_runtime) - - return 0 - - -def parse_args(argv=None): - parser = argparse.ArgumentParser( - description=( - 'Print summary of ShapePipe job status for a given UNIONS patch.' - ), - ) - parser.add_argument( - 'patch', - help='Patch identifier (e.g. P3, P9).', - ) - parser.add_argument( - 'job_exclusive', - nargs='?', - default=None, - help=( - 'Bitmask selecting which jobs to run; ' - + 'if omitted, all jobs run.' - ), - ) - parser.add_argument( - '-v', - '--verbose', - action='store_true', - help='Verbose output.', - ) - return parser.parse_args(argv) - - -def main(argv=None): - args = parse_args(argv) - run(args.patch, args.job_exclusive, args.verbose) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/shapepipe/utilities/__init__.py b/src/shapepipe/utilities/__init__.py index 5f0eab2d6..15e59739c 100644 --- a/src/shapepipe/utilities/__init__.py +++ b/src/shapepipe/utilities/__init__.py @@ -7,4 +7,4 @@ """ -__all__ = ["file_system", "cfis", "galaxy", "summary"] +__all__ = ["file_system", "cfis", "galaxy"] diff --git a/src/shapepipe/utilities/summary.py b/src/shapepipe/utilities/summary.py deleted file mode 100755 index dd5009f3f..000000000 --- a/src/shapepipe/utilities/summary.py +++ /dev/null @@ -1,832 +0,0 @@ -"""SUMMARY - -Author: Martin Kilbinger - -""" - -import sys -import os -import re -import fnmatch - -import logging - -from collections import Counter - -from tqdm import tqdm - - -def init_par_runtime(list_tile_IDs): - - # Numbers updated at runtime - par_runtime = {} - - par_runtime["n_tile_IDs"] = len(list_tile_IDs) - par_runtime["list_tile_IDs"] = list_tile_IDs - - return par_runtime - - -def update_par_runtime_after_find_exp(par_runtime, all_exposures): - - # Single-exposure images - par_runtime["n_exposures"] = len(all_exposures) - par_runtime["list_exposures"] = all_exposures - - # Single-HDU single exposure images - n_CCD = 40 - par_runtime["n_shdus"] = get_par_runtime(par_runtime, "exposures") * n_CCD - par_runtime["list_shdus"] = get_all_shdus(all_exposures, n_CCD) - - return par_runtime - - -def get_IDs_from_file(path): - """Get IDs From File. - - Return IDs from text file. Removes letters and replaces - dots "." with dashes "-". - - Parameters - ---------- - path: str - input file path - - Returns - -------- - list - IDs - - """ - numbers = [] - with open(path) as f_in: - for line in f_in: - entry = line.rstrip() - number = re.sub("[a-zA-Z]", "", entry) - numbers.append(number) - - return numbers - - -def get_all_exposures(exp_number_file_list, verbose=False): - """Get All Exposures. - - Return all exposure names from a list of text files. - - Parameters - ---------- - exp_number_list: list - input file names - - """ - exposures = set() - for idx, path in enumerate(exp_number_file_list): - exps = get_IDs_from_file(path) - exposures.update(exps) - - return list(exposures) - - -def get_all_shdus(exposures, n_CCD): - """Get All SHDUs. - - Return all single-exposure single-HDU (CCD) IDs. - - Parameters - ---------- - exposures: list - exposure names - n_CCD: int - number of CCDs per exposure - - Returns - -------- - list - single-exposure single-HDU IDs - - """ - shdus = [] - for exposure in exposures: - for idx_CCD in range(n_CCD): - shdus.append(f"{exposure}-{idx_CCD}") - - return shdus - - -def set_as_list(item=None, n=None, default=1): - """Set As List. - - Return input as list. - - Parameters - ----------- - item: str, int, or list, optional - input item(s); default is None, in which - case the return is [1] * n - n: int, optional - number of list items to return, default is None, - in which case the number will be set to 1. If item and - n are not None, n has to be equal to len(item) - default: int, optional - value to return if item is not given; - default is 1 - - Raises - ------- - IndexError - if n != len(item) - - Returns - ------- - list - input item(s) as list - """ - my_n = n or 1 - - if not item: - result = [default] * my_n - elif not isinstance(item, list): - result = [item] * my_n - else: - result = item - if len(item) != my_n: - raise IndexError(f"item has length {len(item)} != {n}") - - return result - - -def check_special_one(module, path): - - ngmix_finished = False - - with open(path) as f_in: - lines = f_in.readlines() - for line in lines: - entry = line.rstrip() - - if module == "setools_runner": - m = re.search("Nb stars = (\S*)", line) - if m: - value = int(m[1]) - if value < 2: - code = 0 - msg = ( - f"Not enough stars for random split:" - + f" #stars = {value}" - ) - return msg, code - break - m = re.search("Mode computation failed", line) - if m: - code = 1 - msg = "Mode computation of stellar locus failed" - return msg, code - - if module == "psfex_interp_runner": - m = re.search("Key N_EPOCH not found", line) - if m: - code = 2 - msg = "N_EPOCH not in SEx cat, rerun job 16" - return msg, code - - m = re.search( - "ValueError: cannot reshape array of size 0 into shape", - line, - ) - if m: - code = 3 - msg = "found array of size 0" - return msg, code - - if module == "mask_runner": - m = re.search("Empty or corrupt FITS file", line) - if m: - code = 4 - msg = "empty or corrult FITS file" - return msg, code - - if module == "sextractor_runner": - m = re.search("sextracted 0", line) - if m: - code = 5 - msg = "No object detected (weight might be 0 everywhere)" - return msg, code - m = re.search("astropy\.wcs\.wcs\.NoConvergence", line) - if m: - code = 8 - msg = "WCS world2pix did not converge" - return msg, code - - if module == "ngmix_runner": - m = re.search("finished", line) - if m: - ngmix_finished = True - break - - if module == "merge_sep_cats_runner": - m = re.search("Input catalogue", line) - if m: - code = 7 - msg = "One or more ngmix catalogues not found for merge" - return msg, code - - if module == "ngmix_runner" and not ngmix_finished: - code = 6 - msg = "ngmix incomplete" - return msg, code - - - return None, None - - -class job_data(object): - """Job Data. - - Class to handle a job. - - Parameters - ---------- - bit: int - bit-coded job number - run_dir: str or list - run directory(ies) - modules: list - module names - key_expected: int or str - number of expected output files; if str: will be updated - with runtime value - n_mult: int or list, optional - multiplicity of output files, default `None`, in which - case it is set to 1 - pattern: list, optional - if not None, file pattern to match; defafult is `None` - path_main: str, optional - main (left-most) part of output directory, default is "." - path_left: str, optional - left (first) part of output directory, default is "./output" - output_subdirs: str, optional - output subdirectories if not `None`; default is `None` - path_right: str, optional - right (last) part of output subdir suffix if not `None`; - default is `None` - path_output: str, optional - module output path, default is "output" - output_path_missing_IDs: list, optional - output path of missing ID, if `None` (default) will be - given by job bit and module. - special: bool, optional - if True check output file content for special messages; - default is False - verbose: bool, optional - verbose output if True; default is False - - """ - - def __init__( - self, - bit, - run_dir, - modules, - key_expected, - n_mult=None, - pattern=None, - path_main=".", - path_left="output", - output_subdirs=None, - path_right=None, - path_output="output", - output_path_missing_IDs=None, - special=False, - verbose=False, - ): - self._bit = bit - self._run_dir = set_as_list(item=run_dir, n=len(modules)) - self._modules = modules - self._key_expected = set_as_list(item=key_expected, n=len(modules)) - self._n_mult = set_as_list(item=n_mult, n=len(modules)) - self._pattern = set_as_list(item=pattern, n=len(modules), default="") - self._path_main = path_main - self._path_left = path_left - self._output_subdirs = output_subdirs or [""] - self._path_right = set_as_list( - path_right, - len(modules), - default=".", - ) - self._path_output = set_as_list( - path_output, - len(modules), - default="output", - ) - self._output_path_missing_IDs = output_path_missing_IDs - self._special = set_as_list( - special, - len(modules), - default=False, - ) - self._path_right = set_as_list(path_right, len(modules), default=".") - self._output_path_missing_IDs = output_path_missing_IDs - self._verbose = verbose - - def print_intro(self): - """Print Intro. - - Print header line for job statistics. - - """ - logging.info(f" # Job {self._bit}:") - - @classmethod - def print_stats_header(self): - """Print Stats Header. - - Print overall header information for stats output. - - """ - logging.info( - "module expected found" - + " missing uniq_miss fr_found" - ) - logging.info("=" * 100) - - def print_stats( - self, - module, - n_expected, - n_found, - n_special, - n_missing, - idx, - ): - """Print Stats. - - Print output file statistics. - - Parameters - ---------- - module: str - module name - n_expected: int - number of expected files - n_found: int - number of found files - n_special: int - number of special cases - n_missing: int - number of missing files - idx: int - module index - - """ - module_str = module - - if not self._special[idx]: - if n_expected > 0: - fraction_found = n_found / n_expected - else: - fraction_found = 1 - - n_missing_per_mult = n_missing / self._n_mult[idx] - - else: - module_str = f"{module_str} (special)" - n_found = n_special - n_missing = -1 - n_missing_per_mult = -1 - fraction_found = n_found / n_expected - n_expected = -1 - - logging.info( - f"{module_str:30s} {n_expected:9d} {n_found:9d}" - + f" {n_missing:9d}" - + f" {n_missing_per_mult:9.1f} {fraction_found:9.1%}" - ) - - @classmethod - def is_ID_in_str(self, ID, path): - if ID in path: - return True - - @classmethod - def is_not_in_any(self, ID, list_str): - return not any(ID in string for string in list_str) - - @classmethod - def replace_dot_dash(self, numbers): - - results = [re.sub("\.", "-", number) for number in numbers] - - return results - - @classmethod - def replace_dash_dot_if_tile(self, numbers): - - pattern = re.compile(r"(\d{3})-(\d{3})") - results = [pattern.sub(r"\1.\2", number) for number in numbers] - - return results - - @classmethod - def get_unique(self, names): - n_all = len(names) - names_unique = list(set(names)) - n_unique = len(names_unique) - - if n_all != n_unique: - if True: # self._verbose: - logging.warning( - f"{n_all - n_unique} duplicates removed from {n_all} IDs" - ) - - return names_unique - - @classmethod - def write_IDs_to_file(self, output_path, IDs): - """Write IDs to file. - - Write list if image IDs to text file. - - Parameters - ---------- - output_path: str - output file path - IDs: list - image IDs - - """ - IDs_dot = self.replace_dash_dot_if_tile(IDs) - if len(IDs_dot) > 0: - # Write IDs to file - with open(output_path, "w") as f_out: - for ID in IDs_dot: - print(ID, file=f_out) - elif os.path.exists(output_path): - # Remove preivous obsolete ID file - os.unlink(output_path) - - def check_special(self, module, idx): - - messages = {} - - if self._special[idx]: - - # Loop over input file names and paths - for name, path in zip(self._names_in_dir[idx], self._paths_in_dir[idx]): - - # Check if special case is found - msg, code = check_special_one(module, path) - if msg: - # First time occurance: create empty list for this code - if code not in messages: - messages[code] = [] - - # Append file name, message, and code - messages[code].append(f"{name} {code} {msg}") - - if len(messages) > 0: - # Loop over codes = key in messages dict - for code in messages: - # Create output file for this code - output_path = ( - f"{self._path_main}/summary/special_job_{self._bit}" - + f"_{module}_{code}.txt" - ) - # Write all messages - with open(output_path, "w") as f_out: - for msg in messages[code]: - print(msg, file=f_out) - - # Count all special cases = sum of cases over all codes - n_all = sum([len(messages[code]) for code in messages]) - return n_all - - def output_missing( - self, - module, - idx, - par_runtime=None, - ): - """Output Missing. - - Writes IDs of missing images to disk. - - """ - key_expected = self._key_expected[idx] - names_in_dir = self._names_in_dir[idx] - paths_in_dir = self._paths_in_dir[idx] - n_mult = self._n_mult[idx] - - list_expected = get_par_runtime(par_runtime, key_expected, kind="list") - - # Count image IDs in names that were found earlier - - # Get file name pattern - if module != "split_exp_runner" or (self._bit != 2 and self._bit != 4096): - pattern = re.compile(r"(?:\d{3}-\d{3}|\d{7}-\d+|\d{7})") - else: - # split_exp_runner with sp_local=0: input is exp, output is shdu - # (images) and exp (header); ignore hdu number. - # If sp_local=1 set bit to != 2 - # Update 11/2025: No longer working for P9 4096. Solution: set n_mult=3. - pattern = re.compile( - r"(?:\d{3}-\d{3}|\d{7})" - ) - - ## Extract image IDs from names - IDs = [] - for name, path in zip(names_in_dir, paths_in_dir): - - match = pattern.search(name) - if match: - ID = match.group() - IDs.append(ID) - else: - msg = f"No ID found in {name}" - #raise ValueError(msg) - print(f"Warning: {msg}, continuing") - - # For split_exp_runner P8, IDs now contain exps and sdus, - # not matching mult. - - ## Count occurences - ID_counts = Counter(IDs) - - ## Add to missing if ocurence less than n_mult - missing_IDs = [] - for ID in list_expected: - if ID_counts[ID] < n_mult: - missing_IDs.append(ID) - - n_all = len(missing_IDs) - missing_IDs_unique = self.get_unique(missing_IDs) - - if not self._output_path_missing_IDs: - # Default name using bit and module - output_path = ( - f"{self._path_main}/summary/missing_job_{self._bit}" - + f"_{module}.txt" - ) - else: - # User-defined name (e.g. ngmix_runner_X) - output_path = self._output_path_missing_IDs[idx] - self.write_IDs_to_file(output_path, missing_IDs_unique) - - return missing_IDs_unique - - def output_missing_job(self): - output_path = ( - f"{self._path_main}/summary/missing_job_{self._bit}_all.txt" - ) - - missing_IDs_all = set(self._missing_IDs_job) - - self.write_IDs_to_file(output_path, missing_IDs_all) - - @classmethod - def get_last_full_path(self, base_and_subdir, matches): - """Get Last Full Path - - Return full path of last file in list. - - """ - # Sort according to creation time - matches_sorted = sorted( - matches, - key=lambda entry: entry.name, - ) - - # Get most recent one - last = matches_sorted[-1] - - # Get full path - full_path = os.path.join(base_and_subdir, last.name) - - return full_path - - @classmethod - def get_module_output_dir(self, full_path, module, path_output): - """Get Module Output Dir. - - Return output directory name for given module. - - """ - directory = f"{full_path}/{module}/{path_output}" - - return directory - - def get_matches_final(self, directory, idx): - - # Loop over files - # os.path.whether exists is twice faster than try/except - - if os.path.exists(directory): - pattern = f"{self._pattern[idx]}*" - for entry2 in os.scandir(directory): - if ( - entry2.is_file() - and (fnmatch.fnmatch(entry2.name, pattern)) - and entry2.stat().st_size > 0 - ): - # Append matching files - self._names_in_dir[idx].append(entry2.name) - self._paths_in_dir[idx].append( - os.path.join(directory, entry2.name) - ) - - def get_names_in_dir(self, iterable, module, idx): - - # Initialise output file names and paths - self._names_in_dir[idx] = [] - self._paths_in_dir[idx] = [] - - # Loop over subdirs - for jdx, subdir in enumerate(iterable): - base_and_subdir = ( - f"{self._path_main}/" - + f"{self._path_left}/{subdir}/" - + f"{self._path_right[idx]}" - ) - if self._verbose: - print(f"**** base_and_subdir {base_and_subdir}") - - if os.path.isdir(base_and_subdir): - - matches = [] - - # Loop over entries (files and dirs) - with os.scandir(base_and_subdir) as entries: - for entry in entries: - - # Append directory name if matches module - if ( - entry.name.startswith(self._run_dir[idx]) - and not entry.name.endswith("prev") - ): - matches.append(entry) - - # This entry does not match module -> next - if not matches: - continue - - if self._verbose: - print("**** Matching entries: ", end="") - for match in matches: - print(match.name) - - full_path = self.get_last_full_path( - base_and_subdir, - matches, - ) - - # Get module output directory - directory = self.get_module_output_dir( - full_path, - module, - self._path_output[idx], - ) - if self._verbose: - print(f"**** Output dir = {directory}") - - # Find matching file names and paths - self.get_matches_final(directory, idx) - else: - if self._verbose: - print(f"Directory {base_and_subdir} not found") - - def update_subdirs(self, par_runtime): - """Update Subdirs. - - Update subdir names with runtime information if required. - - """ - if not isinstance(self._output_subdirs, list): - self._output_subdirs = get_par_runtime( - par_runtime, self._output_subdirs, kind="list" - ) - - def check_numbers(self, par_runtime=None, indices=None): - """Check Numbers. - - Check output file numbers and IDs. - - Parameters - ---------- - par_runtime : dict, optional - runtime parameter. default is None - indices: list, optional - if not None (default), only check modules corresponding - to indices - - """ - # Update subdirs if not already set as list - self.update_subdirs(par_runtime) - - # Initialise variables - self._names_in_dir = {} - self._paths_in_dir = {} - self._missing_IDs_job = [] - n_missing_job = 0 - - # Loop over modules - for idx, module in enumerate(self._modules): - if indices is not None and idx not in indices: - continue - - if self._verbose: - print(f"** module {module}") - - # Look over subdirs - iterable = self._output_subdirs - if len(iterable) > 1 and self._verbose: - iterable = tqdm(iterable, desc="subdirs", leave=False) - - if self._verbose: - print(f"*** subdirs {self._output_subdirs}") - - # Get output file names and paths - self.get_names_in_dir( - iterable, - module, - idx, - ) - - # If expected is string: Update parameter with runtime value - # and set as integer - if isinstance(self._key_expected[idx], str): - n_expected_base = get_par_runtime( - par_runtime, self._key_expected[idx], kind="n" - ) - else: - n_expected_base = self._key_expected[idx] - - # Get some numbers - n_found = len(self._names_in_dir[idx]) - n_expected = n_expected_base * self._n_mult[idx] - n_missing = n_expected - n_found - - n_special = self.check_special( - module, - idx, - ) - - # Print statistics - self.print_stats( - module, - n_expected, - n_found, - n_special, - n_missing, - idx, - ) - - # Write missing IDs for module to file - if n_missing > 0: - missing_IDs = self.output_missing( - module, - idx, - par_runtime=par_runtime, - ) - n_missing_job += n_missing - self._missing_IDs_job.extend(missing_IDs) - - # Empty line after job - logging.info("") - - # Write missing IDs for entire job to file - # if n_missing_job > 0: - self.output_missing_job() - - -def get_par_runtime(par_runtime, key, kind="n"): - """Get Par RunTime. - - Return runtime parameter value. - - Parameters - ---------- - par_runtime: dict - runtime parameter - key: str - key - - """ - combined_key = f"{kind}_{key}" - - return par_runtime[combined_key] - - -def print_par_runtime(par_runtime, verbose=True): - # Print runtime parameter values - if True: - logging.info("") - logging.info("===========") - logging.info("par_runtime") - logging.info("-----------") - for key, value in par_runtime.items(): - if not key.startswith("list"): - logging.info(f"{key:30s} {value:6d}") - else: - # logging.info(f"{key:30s} {len(value):6d} entries") - pass - logging.info("===========") - logging.info("") diff --git a/src/shapepipe/utilities/summary_params_pre_v2.py b/src/shapepipe/utilities/summary_params_pre_v2.py deleted file mode 100644 index 2def4d33b..000000000 --- a/src/shapepipe/utilities/summary_params_pre_v2.py +++ /dev/null @@ -1,276 +0,0 @@ -# Parameters for summary run - -import logging -import os - -from shapepipe.utilities import summary - - -def set_jobs_v2_pre_v2(patch, verbose): - """Return information about shapepipe jobs""" - print(f"Set job info for patch {patch}") - - # Main input and output directory - path_main = f"{os.environ['HOME']}/cosmostat/v2/pre_v2/psfex/{patch}" - - # Logging - path = f"{path_main}/summary" - if not os.path.isdir(path): - os.mkdir(path) - log_file_name = f"{path}/summary_log.txt" - handlers = [ - logging.FileHandler(log_file_name, mode="w"), - logging.StreamHandler(), - ] - logging.basicConfig( - level=logging.INFO, format="%(message)s", handlers=handlers - ) - - logging.info(f"Checking main directory = {path_main}") - - # Tile IDs - tile_ID_path = f"{path_main}/tile_numbers.txt" - - ## Tile IDs with dots - list_tile_IDs_dot = summary.get_IDs_from_file(tile_ID_path) - - jobs = {} - - # Set the first job (retrieve images) - - # With "CFIS_" only the linked images are counted. The original - # ones do not match the IDdash pattern. - # If images were downloaded in several runs: - # - Only copy original images, then (re-)set links in SP numbering format - # - get_images_runner_run_[12] consistent - # - remove previous output dirs since only last is searched - jobs["1"] = summary.job_data( - 1, - "run_sp_GitFeGie", - [ - "get_images_runner_run_1", - "find_exposures_runner", - "get_images_runner_run_2", - ], - ["tile_IDs", "tile_IDs", "exposures"], - pattern=["CFIS_", "", ""], - n_mult=[2, 1, 3], - path_main=path_main, - path_left="output", - verbose=verbose, - ) - - if patch in ("P8", "P9"): - jobs["2"] = summary.job_data( - 2, - ["run_sp_Uz", "run_sp_exp_Sp_shdu"], - ["uncompress_fits_runner", "split_exp_runner"], - ["tile_IDs", "shdus"], - n_mult=[1, 3], - path_main=path_main, - path_left=["output", "exp_runs"], - output_subdirs=[None, "shdus"], - path_right=[None, "output"], - verbose=verbose, - ) - jobs["4096"] = summary.job_data( - 4096, - ["run_sp_exp_Sp_shdu"], - ["split_exp_runner"], - ["shdus"], - n_mult=4, - path_main=path_main, - path_left="exp_runs", - output_subdirs="shdus", - path_right="output", - verbose=verbose, - ) - else: - jobs["2"] = summary.job_data( - 2, - ["run_sp_Uz", "run_sp_exp_SpMh"], - ["uncompress_fits_runner", "split_exp_runner"], - ["tile_IDs", "shdus"], - n_mult=[1, 121], - path_main=path_main, - path_left="output", - verbose=verbose, - ) - - jobs["4"] = summary.job_data( - 4, - ["run_sp_Ma_tile"], - ["mask_runner"], - ["tile_IDs"], - path_main=path_main, - path_left="output", - verbose=verbose, - ) - - if patch not in ("P8", "P9"): - jobs["8"] = summary.job_data( - 8, - ["run_sp_Ma_exp"], - ["mask_runner"], - ["shdus"], - path_main=path_main, - path_left="output", - verbose=verbose, - ) - else: - jobs["8"] = summary.job_data( - 8, - ["run_sp_exp_Ma"], - ["mask_runner"], - ["shdus"], - n_mult=[1], - path_main=path_main, - path_left="exp_runs", - output_subdirs= "shdus", - path_right="output", - verbose=verbose, - ) - - - jobs["16"] = summary.job_data( - 16, - "run_sp_tile_Sx", - ["sextractor_runner", "sextractor_runner"], - "tile_IDs", - n_mult=[2, 1], - path_main=path_main, - path_left="tile_runs", - path_output=["output", "logs"], - output_subdirs=[f"{tile_ID}/output" for tile_ID in list_tile_IDs_dot], - special=[False, True], - verbose=verbose, - ) - - jobs["32"] = summary.job_data( - 32, - [ - "run_sp_exp_SxSePsf", - "run_sp_exp_SxSePsf", - "run_sp_exp_SxSePsf", - "run_sp_exp_SxSePsf", - "run_sp_exp_SxSePsf", - ], - [ - "sextractor_runner", - "setools_runner", - "setools_runner", - "setools_runner", - "psfex_runner", - ], - "shdus", - n_mult=[2, 2, 1, 1, 2], - path_main=path_main, - path_left="exp_runs", - output_subdirs="shdus", - path_right="output", - path_output=[ - "output", - "output/rand_split", - "output/stat", - "logs", - "output", - ], - special=[False, False, True, True, False], - verbose=verbose, - ) - - jobs["64"] = summary.job_data( - "64", - "run_sp_tile_PsViSmVi", - [ - "psfex_interp_runner", - "psfex_interp_runner", - "vignetmaker_runner_run_1", - "spread_model_runner", - "vignetmaker_runner_run_2", - ], - "tile_IDs", - n_mult=[1, 1, 1, 1, 4], - path_main=path_main, - path_left="tile_runs", - output_subdirs=[f"{tile_ID}/output" for tile_ID in list_tile_IDs_dot], - path_output=["output", "logs", "output", "output", "output"], - special=[False, True, False, False, False], - verbose=verbose, - ) - - if patch in ("P2", "P5", "P8", "P9"): - n_sh = 1 - else: - n_sh = 8 - run_dirs = [f"run_sp_tile_ngmix_Ng{idx+1}u" for idx in range(n_sh)] - - # Add special (unfinished run) - run_dirs.append("run_sp_tile_ngmix_Ng1u") - - output_path_missing_IDs = [ - f"{path_main}/summary/missing_job_128_ngmix_runner_{idx+1}.txt" - for idx in range(n_sh + 1) - ] - jobs["128"] = summary.job_data( - "128", - run_dirs, - ["ngmix_runner"] * (n_sh + 1), - "tile_IDs", - path_main=path_main, - path_left="tile_runs", - output_subdirs=[f"{tile_ID}/output" for tile_ID in list_tile_IDs_dot], - path_output=["output"] * n_sh + ["logs"], - special=[False] * n_sh + [True], - output_path_missing_IDs=output_path_missing_IDs, - verbose=verbose, - ) - - jobs["256"] = summary.job_data( - "256", - "run_sp_Ms", - ["merge_sep_cats_runner"] * 2, - "tile_IDs", - path_main=path_main, - path_left="tile_runs", - path_output=["output", "logs"], - special=[False, True], - output_subdirs=[f"{tile_ID}/output" for tile_ID in list_tile_IDs_dot], - verbose=verbose, - ) - - jobs["512"] = summary.job_data( - "512", - ["run_sp_Mc"], - ["make_cat_runner"], - "tile_IDs", - path_main=path_main, - path_left="tile_runs", - output_subdirs=[f"{tile_ID}/output" for tile_ID in list_tile_IDs_dot], - verbose=verbose, - ) - - # Post-processing - jobs["1024"] = summary.job_data( - "1024", - ["run_sp_combined_final"], - ["make_catalog_runner"], - "tile_IDs", - path_main=path_main, - path_left="output", - verbose=verbose, - ) - - jobs["2048"] = summary.job_data( - "2048", - "run_sp_combined_psf", - ["psfex_interp_runner"], - "shdus", - path_main=path_main, - path_left="output", - verbose=verbose, - ) - - return jobs, list_tile_IDs_dot - - diff --git a/tests/unit/test_exp_footprint.py b/tests/unit/test_exp_footprint.py index 44da97a00..5ee569413 100644 --- a/tests/unit/test_exp_footprint.py +++ b/tests/unit/test_exp_footprint.py @@ -14,7 +14,7 @@ silently WRONG — right pixels, wrong exposure count — if they ever diverged by a permutation. This module is where that contract is pinned. It inherits the role of ``tests/module/test_coverage.py::test_get_all_shdus``, which pinned the same -``-`` format against the summary scrape that is being retired. +``-`` format against the summary scrape, both now retired. The fixtures are real astropy WCSs, one per CCD with a DISTINCT centre, so a permutation of the array shows up as corners on the wrong ``id`` rather than as From 75aa69426af5e86dca4bbc6c2a8dc2f00fbfb855 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:26:52 +0200 Subject: [PATCH 21/36] style: wrap the stripped coverage module to the 79-col line length Also sorts coverage_map_builder's import block, which the CLI's cs_util imports had been hiding. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- src/shapepipe/utilities/coverage_map_builder.py | 15 ++++++++------- tests/module/test_coverage.py | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/shapepipe/utilities/coverage_map_builder.py b/src/shapepipe/utilities/coverage_map_builder.py index 43584c94e..659a0d71f 100644 --- a/src/shapepipe/utilities/coverage_map_builder.py +++ b/src/shapepipe/utilities/coverage_map_builder.py @@ -19,9 +19,9 @@ """ -import numpy as np import healsparse as hsp import hpgeom as hpg +import numpy as np # Declination beyond which a polygon is considered too close to a pole for # HealSparse's planar polygon fill to be reliable. CCD footprints are ~10 @@ -156,14 +156,15 @@ def build_map( def median_filter(hsp_map, n_iterations=1): - """Smooth an nexp map by replacing each pixel with its neighbourhood median. + """Smooth an nexp map: each pixel becomes its neighbourhood median. The neighbourhood is a pixel and its eight HEALPix neighbours; out-of-map - neighbours read as the map's sentinel and so pull an edge pixel down, which - is the intended behaviour for a coverage map (a lone pixel is noise, not - depth). The ``coverage_map`` rule does NOT smooth — the map it writes is the - raw exposure count, which is what sp_validation's mask application wants — - so this is the offline step, for looking at a map rather than applying one. + neighbours read as the map's sentinel and so pull an edge pixel down, + which is the intended behaviour for a coverage map (a lone pixel is noise, + not depth). The ``coverage_map`` rule does NOT smooth — the map it writes + is the raw exposure count, which is what sp_validation's mask application + wants — so this is the offline step, for looking at a map rather than + applying one. Parameters ---------- diff --git a/tests/module/test_coverage.py b/tests/module/test_coverage.py index f71050be4..26d98a809 100644 --- a/tests/module/test_coverage.py +++ b/tests/module/test_coverage.py @@ -150,7 +150,7 @@ def test_check_nside_rejects_non_power_of_two(nside_coverage, nside): def test_build_map_validates_nside_before_stamping(): - """``build_map`` refuses a bad nside rather than failing deep in healsparse.""" + """``build_map`` refuses a bad nside before it reaches healsparse.""" with pytest.raises(ValueError, match="nside"): build_map(["100-0"], [0.0, 0.1, 0.1, 0.0], [0.0, 0.0, 0.1, 0.1], 32, 3000) @@ -171,7 +171,7 @@ def test_build_map_single_footprint(): def test_build_map_nexp_counts_overlapping_exposures(): - """Two overlapping CCDs from different exposures give value 2 in overlap.""" + """Two overlapping CCDs from two exposures give value 2 in the overlap.""" # Two 0.4x0.4 deg CCDs offset by 0.2 deg in RA -> a central overlap strip. m = build_map( ["100-0", "200-0"], @@ -244,7 +244,7 @@ def test_build_map_pole_guard_skips_polygon(capsys): # --------------------------------------------------------------------------- def test_median_filter_fills_a_lone_hole(): - """A single low pixel inside a covered disc is pulled up to its neighbours.""" + """A lone low pixel in a covered disc is pulled up to its neighbours.""" nside = 1024 m = hsp.HealSparseMap.make_empty(32, nside, np.uint16) m[hpg.query_circle(nside, 10.0, 20.0, 0.1)] = 2 From 033b69ddd576f3a0fab92c5efebc62e16ccc0ed2 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:30:47 +0200 Subject: [PATCH 22/36] refactor: retire the canfar-VM generation of run scripts The 2020-2022 CANFAR VM / HTCondor layer, superseded by the Snakemake workflow and already broken in several places: - canfar_submit_selection.sh writes HTCondor submit files naming job_sp.bash, which has not existed in the tree for years. - canfar_download_results.bash pulled per-tile .tgz back from vos:cfis/cosmostat/kilbinger/results; products now land on products_dir directly. - post_proc_sp.bash chains combine_runs -t psf (an option combine_runs never parsed) and untar_results.sh (absent). - canfar_avail_results.py counted an expected result-name set off a vos listing; `sp report` reads the manifests instead. - stats_headless_canfar.py printed its own deprecation notice and was the sole importer of skaha; canfar_monitor -q replaces it. - merge_final_cat.py was superseded by create_final_cat.py per its own docstring, and its input was a combine_runs.bash link farm. skaha therefore loses its last importer: dropped from pyproject, from the dependencies table, and (surgically, to avoid an unrelated marker rewrite from a newer uv) from uv.lock. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- docs/source/dependencies.md | 1 - pyproject.toml | 1 - scripts/python/canfar_avail_results.py | 427 ------------------------ scripts/python/merge_final_cat.py | 396 ---------------------- scripts/python/stats_headless_canfar.py | 29 -- scripts/sh/canfar_download_results.bash | 128 ------- scripts/sh/canfar_submit_selection.sh | 119 ------- scripts/sh/post_proc_sp.bash | 86 ----- uv.lock | 20 -- 9 files changed, 1207 deletions(-) delete mode 100755 scripts/python/canfar_avail_results.py delete mode 100755 scripts/python/merge_final_cat.py delete mode 100755 scripts/python/stats_headless_canfar.py delete mode 100755 scripts/sh/canfar_download_results.bash delete mode 100755 scripts/sh/canfar_submit_selection.sh delete mode 100755 scripts/sh/post_proc_sp.bash diff --git a/docs/source/dependencies.md b/docs/source/dependencies.md index 9378bfe00..bf595346b 100644 --- a/docs/source/dependencies.md +++ b/docs/source/dependencies.md @@ -35,7 +35,6 @@ Data access & infrastructure (CANFAR / UNIONS): | Package | Purpose | |---------|---------| | [vos](https://github.com/opencadc/vostools) | CADC / CANFAR VOSpace access | -| [skaha](https://github.com/shinybrar/skaha) | CANFAR Science Platform sessions | | canfar | CANFAR container-job submission | | [astroquery](https://astroquery.readthedocs.io/) | external catalogue queries | | [cs_util](https://github.com/CosmoStat/cs_util) | shared CosmoStat utilities | diff --git a/pyproject.toml b/pyproject.toml index c90eb908d..f845a2621 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,6 @@ dependencies = [ "pyqtgraph", "reproject>=0.19", "sf_tools>=2.0.4", - "skaha>=1.7", "skyproj", "sqlitedict>=2.0", "termcolor", diff --git a/scripts/python/canfar_avail_results.py b/scripts/python/canfar_avail_results.py deleted file mode 100755 index 62fbf0564..000000000 --- a/scripts/python/canfar_avail_results.py +++ /dev/null @@ -1,427 +0,0 @@ -#!/usr/bin/env python - -"""Script canfar_avail_results.py - -Check whether results files are available on vos. - -:Author: Martin Kilbinger - -:Date: 07/2020 -""" - -import re -import os -import sys -import glob -import copy -import io -from contextlib import redirect_stdout - -from optparse import OptionParser - -from cs_util.canfar import dir_list - -from shapepipe.utilities import cfis - - -def params_default(): - """Set default parameter values. - - Parameters - ---------- - None - - Returns - ------- - p_def: class cfis.param - parameter values - """ - - p_def = cfis.param( - input_IDs=".", - input_path="vos:cfis/cosmostat/kilbinger/results", - psf="mccd", - extension="tgz", - ) - - return p_def - - -def parse_options(p_def): - """Parse command line options. - - Parameters - ---------- - p_def: class cfis.param - parameter values - - Returns - ------- - options: tuple - Command line options - args: string - Command line string - """ - - usage = "%prog [OPTIONS]" - parser = OptionParser(usage=usage) - - # I/O - parser.add_option( - "-i", - "--input_IDs", - dest="input_IDs", - type="string", - default=p_def.input_IDs, - help="input tile ID file(s) or directory path, default='{}'".format( - p_def.input_IDs - ), - ) - parser.add_option( - "", - "--input_path", - dest="input_path", - type="string", - default=p_def.input_path, - help="input path, local or vos url, default='{}'".format( - p_def.input_path - ), - ) - parser.add_option( - "-o", - "--output_not_avail", - dest="output_not_avail", - type="string", - help="output file for not-available IDs, default no output", - ) - - parser.add_option( - "-p", - "--psf", - dest="psf", - type="string", - default=p_def.psf, - help="PSF model, one in ['psfex'|'mccd'], default='{}'".format( - p_def.psf - ), - ) - - parser.add_option( - "-f", - "--final_only", - dest="final_only", - action="store_true", - help="only check final catalogues", - ) - parser.add_option( - "-m", - "--mask_only", - dest="mask_only", - action="store_true", - help="only check mask files (pipeline_flag)", - ) - parser.add_option( - "-x", - "--extension", - dest="extension", - type="string", - default=p_def.extension, - help=f"file extension, default='{p_def.extension}'", - ) - parser.add_option( - "-v", - "--verbose", - dest="verbose", - action="store_true", - help="verbose output", - ) - - options, args = parser.parse_args() - - return options, args - - -def check_options(options): - """Check command line options. - - Parameters - ---------- - options: tuple - Command line options - - Returns - ------- - erg: bool - Result of option check. False if invalid option value. - """ - - if options.psf not in ["psfex", "mccd"]: - print("Invalid PSF model '{}'".format(options.psf)) - return False - - if options.final_only and options.mask_only: - print("One one of the options '-f' or '-m' can be given") - return False - - return True - - -def update_param(p_def, options): - """Return default parameter, updated and complemented according to options. - - Parameters - ---------- - p_def: class param - parameter values - optiosn: tuple - command line options - - Returns - ------- - param: class param - updated paramter values - """ - - param = copy.copy(p_def) - - # Update keys in param according to options values - for key in vars(param): - if key in vars(options): - setattr(param, key, getattr(options, key)) - - # Add remaining keys from options to param - for key in vars(options): - if not key in vars(param): - setattr(param, key, getattr(options, key)) - - # Do extra stuff if necessary - - return param - - -def read_input_files(input_path, verbose=False): - """Return list of ID files. - - Parameters - ---------- - input_path: string - list of files or directory name - verbose: bool, optional, default=False - verbose output if True - - Returns - ------- - ID_files: list of strings - file names with tile IDs - """ - - if os.path.isdir(input_path): - input_files = glob.glob("{}/*".format(input_path)) - else: - input_files = cfis.my_string_split(input_path, stop=True, sep=" ") - - ID_files = [] - for f in input_files: - if os.path.isdir(f): - if verbose: - print("Skipping directory '{}'".format(f)) - else: - ID_files.append(f) - - if verbose: - print("{} input files found".format(len(ID_files))) - - return ID_files - - -def check_results( - ID_files, - input_path, - result_base_names, - n_complete, - extension, - verbose=False, -): - """Count the number of result files uploaded to vos for each input ID file. - - Parameters - ---------- - ID_files: list of strings - file name with tile IDs - input_path: string - path input directory - result_base_names: list of strings - result file base names - n_complete: int - number of files for complete result set - verbose: bool, optional, default=False - verbose output if True - - Returns - ------- - n_found: dictionary - number of files found for each input file and each ID - n_IDs: dictionary - number of ID for each input file - IDs_not_avail: list - IDs that are not available on vos - """ - - m = re.match("vos:", input_path) - if m: - ls_tmp = dir_list(input_path) - else: - ls_tmp = glob.glob(f"{input_path}/*") - ls_out = [os.path.basename(path) for path in ls_tmp] - - n_found = {} - n_IDs = {} - IDs_not_avail = [] - - # Loop over all input files - for ID_list in ID_files: - with open(ID_list) as f: - if verbose: - print("Checking ID list file {}...".format(ID_list)) - n_found[ID_list] = {} - n_IDs[ID_list] = 0 - - # Loop over all lines = IDs in file - for line in f: - ID = line.rstrip() - n_found[ID_list][ID] = 0 - - if extension == "fits": - ID_fname = ID.replace(".", "-") - ID_fname = f"-{ID_fname}" - else: - ID_fname = f"_{ID}" - # Count how many result files are available - for base in result_base_names: - name = "{}{}.{}".format(base, ID_fname, extension) - if name in ls_out: - n_found[ID_list][ID] = n_found[ID_list][ID] + 1 - n_IDs[ID_list] = n_IDs[ID_list] + 1 - - # If not complete set found, add to not-avail list - if n_found[ID_list][ID] != n_complete: - IDs_not_avail.append(ID) - - return n_found, n_IDs, IDs_not_avail - - -def output_summary(n_found, n_IDs, n_complete): - """Create output with summary of result availability. - - Parameters - ---------- - n_found: dictionary - number of files found for each input file and each ID - n_IDs: dictionary - number of ID for each input file - n_complete: int - number of files of a complete set of results - """ - - for ID_list in n_found.keys(): - nf = sum(value == n_complete for value in n_found[ID_list].values()) - print( - "{}: {}/{} ({:.1f}%) complete".format( - os.path.basename(ID_list), - nf, - n_IDs[ID_list], - nf / n_IDs[ID_list] * 100, - ) - ) - - -def output_IDs(ID_list, output): - """Write IDs to file - - Parameters - ---------- - ID_list: list of string - IDs - output: string - output file name - """ - - f = open(output, "w") - for ID in ID_list: - print(ID, file=f) - f.close() - - -def main(argv=None): - - # Set default parameters - p_def = params_default() - - # Command line options - options, args = parse_options(p_def) - - if check_options(options) is False: - return 1 - - param = update_param(p_def, options) - - # Save calling command - cfis.log_command(argv) - if param.verbose: - cfis.log_command(argv, name="sys.stdout") - - ### Start main program ### - - if param.verbose: - print("Start of program {}".format(os.path.basename(argv[0]))) - - ID_files = read_input_files(param.input_IDs, verbose=param.verbose) - - if param.final_only: - result_base_names = ["final_cat"] - elif param.mask_only: - result_base_names = ["pipeline_flag"] - else: - result_base_names = [] - types = [ - "final_cat", - "pipeline_flag", - "logs", - "setools_mask", - "setools_stat", - "setools_plot", - ] - for t in types: - result_base_names.append(t) - - if param.psf == "psfex": - result_base_names.append("psfex_interp_exp") - elif param.psf == "mccd": - result_base_names.append("mccd_fit_val_runner") - - n_complete = len(result_base_names) - - n_found, n_IDs, IDs_not_avail = check_results( - ID_files, - param.input_path, - result_base_names, - n_complete, - param.extension, - verbose=param.verbose, - ) - - output_summary(n_found, n_IDs, n_complete) - - if param.output_not_avail: - output_IDs(IDs_not_avail, param.output_not_avail) - - ### End main program - - if param.verbose: - print("End of program {}".format(os.path.basename(argv[0]))) - - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/scripts/python/merge_final_cat.py b/scripts/python/merge_final_cat.py deleted file mode 100755 index 9d9c10d21..000000000 --- a/scripts/python/merge_final_cat.py +++ /dev/null @@ -1,396 +0,0 @@ -#!/usr/bin/env python - -# -*- coding: utf-8 -*- - -"""Script merge_final_cat.py - -Merge all final catalogues, created by ShapePipe module -``make_catalogue_runner``, into a joined numpy binary file. - -:Authors: Axel Guinot, Martin Kilbinger - -""" - -from astropy.io import fits -import numpy as np -import os -import sys -import re -import copy - -from optparse import OptionParser - -from shapepipe.utilities import cfis - - -class param: - """General class to store (default) variables""" - - def __init__(self, **kwds): - self.__dict__.update(kwds) - - def print(self, **kwds): - print(self.__dict__) - - def var_list(self, **kwds): - return vars(self) - - -def params_default(): - """Params Default. - - Set default parameter values. - - Returns - ------- - class param - parameter values - - """ - p_def = param( - input_path=".", - input_name_base="final_cat", - hdu_num=1, - ) - - return p_def - - -def parse_options(p_def): - """Parse Options. - - Parse command line options. - - Parameters - ---------- - p_def: class param - parameter values - - Returns - ------- - list - command line options - command line str - - """ - usage = "%prog [OPTIONS]" - parser = OptionParser(usage=usage) - - # IO - parser.add_option( - "-i", - "--input_path", - dest="input_path", - type="string", - default=p_def.input_path, - help=f"input path, default='{p_def.input_path}'", - ) - parser.add_option( - "-n", - "--input_name_base", - dest="input_name_base", - type="string", - default=p_def.input_name_base, - help=f"input name base, default='{p_def.input_name_base}'", - ) - parser.add_option( - "-l", - "--list_tile_ID_path", - dest="tile_ID_list_path", - type="string", - default=None, - help=f"tile ID list, default: Use all data in input files", - ) - - # Control - parser.add_option( - "-p", - "--param_path", - dest="param_path", - type="string", - default=None, - help="parameter file path, default=None", - ) - - parser.add_option( - "", - "--hdu_num", - dest="hdu_num", - type="int", - default=p_def.hdu_num, - help=f"input HDU number, default='{p_def.hdu_num}'", - ) - - parser.add_option( - "-v", - "--verbose", - dest="verbose", - action="store_true", - help="verbose output", - ) - - options, args = parser.parse_args() - - return options, args - - -def check_options(options): - """Check Options. - - Check command line options. - - Parameters - ---------- - options: tuple - Command line options - - Returns - ------- - bool - Result of option check. False if invalid option value. - - """ - return True - - -def update_param(p_def, options): - """Update Param. - - Return default parameter, updated and complemented according to options. - - Parameters - ---------- - p_def: class param - parameter values - optiosn: tuple - command line options - - Returns - ------- - class param - updated paramter values - - """ - param = copy.copy(p_def) - - # Update keys in param according to options values - for key in vars(param): - if key in vars(options): - setattr(param, key, getattr(options, key)) - - # Add remaining keys from options to param - for key in vars(options): - if not key in vars(param): - setattr(param, key, getattr(options, key)) - - # Do extra stuff if necessary - - return param - - -# MKDEBUG TODO: remove this function, duplicate in create_final_cat.py -def read_param_file(path, verbose=False): - """Read Param File. - - Return parameter list read from file. - - Parameters - ---------- - path: str - input file name - verbose: bool, optional, default=False - verbose output if True - - Returns - ------- - list of str - parameter names - - """ - param_list = [] - - if path: - - with open(path) as f: - for line in f: - if line.startswith("#"): - continue - entry = line.rstrip() - if not entry or entry == "": - continue - param_list.append(entry) - - if verbose: - if len(param_list) > 0: - print(f"Copying {len(param_list)} columns", end="") - else: - print("Copying all columns", end="") - print(" into merged catalogue") - - # Check for multiples - multiples = [] - for param in param_list: - if param_list.count(param) > 1: - multiples.append(param) - - if len(multiples) > 0: - print( - "The following parameters are more than one times " - "in the parameter file: ", - end="", - ) - for m in multiples: - print(m, end=" ") - print() - raise ValueError("Multiple identical keys found") - - return param_list - - -def get_data(path, hdu_num, param_list): - """Get Data. - - Return data of selected columns from FITS file. - - Parameters - ---------- - path: str - input file name - hdu_num: int - HDU number - param_list: list of str - parameters to be extracted. If none, copy - all columns - - Returns - ------- - numpy array - data columns - - """ - hdu_list = fits.open(path) - hdu = hdu_list[hdu_num] - - if param_list: - cols = [] - for p in param_list: - cols.append(hdu.columns[p]) - coldefs = fits.ColDefs(cols) - hdu_new = fits.BinTableHDU.from_columns(coldefs) - d = hdu_new.data - else: - d = hdu.data - - return d - - -def main(argv=None): - - - # Set default parameters - p_def = params_default() - - # Command line options - options, args = parse_options(p_def) - - if check_options(options) is False: - return 1 - - param = update_param(p_def, options) - - if param.verbose: - print("Start") - - # Save command line arguments to log file - f_log = cfis.log_command(argv, close_no_return=False) - - path = param.input_path - - if param.verbose: - print("Read parameter file") - param.param_list = read_param_file(param.param_path, verbose=param.verbose) - - # read (optional) input tile ID file - if param.tile_ID_list_path: - if param.verbose: - print("Read tile ID list") - tile_ID_list = cfis.read_list(param.tile_ID_list_path) - - if param.verbose: - print("Find input catalogue FITS files") - l = os.listdir(path=path) - ext = "fits" - lpath = [] - for this_l in l: - - add_this_l = False - - # mark to add if correct extension, matches input pattern, not `.npy` file - if ( - this_l.endswith(ext) - and (f"{param.input_name_base}" in this_l) - and (".npy" not in this_l) - ): - add_this_l = True - - # unmark to add if no in (optional) input tile ID file - if param.tile_ID_list_path: - nix, niy = cfis.get_tile_number(this_l) - tile_ID = f"{nix}.{niy}" - if tile_ID not in tile_ID_list: - add_this_l = False - if add_this_l: - lpath.append(os.path.join(path, this_l)) - - if param.verbose: - print(f"{len(lpath)} files files to merge found") - - count = 0 - - # Determine number of columns and keys from first catalogue file - d_tmp = get_data(lpath[0], param.hdu_num, param.param_list) - d = np.zeros(d_tmp.shape, dtype=d_tmp.dtype) - for key in d_tmp.dtype.names: - d[key] = d_tmp[key] - count = count + 1 - if param.verbose: - print(f"File '{lpath[0]}' copied ({count}/{len(lpath)})") - - # merge remaining catalogue files - for fname in lpath[1:]: - - try: - d_tmp = get_data(fname, param.hdu_num, param.param_list) - dd = np.zeros(d_tmp.shape, dtype=d.dtype) - - for key in d_tmp.dtype.names: - dd[key] = d_tmp[key] - - count = count + 1 - if param.verbose: - print(f"File '{fname}' copied ({count}/{len(lpath)})") - - d = np.concatenate((d, dd)) - except: - print( - f"Error while adding file '{fname}', {len(dd)} objects" - " not in final cat" - ) - - # Save merged catalogue as numpy binary file - if param.verbose: - print("Saving merged catalogue") - np.save(f"{param.input_name_base}.npy", d) - - msg = f"{count} catalog files merged with success" - if param.verbose: - print(msg) - print(msg, file=f_log) - - f_log.close() - - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/scripts/python/stats_headless_canfar.py b/scripts/python/stats_headless_canfar.py deleted file mode 100755 index 1f728949d..000000000 --- a/scripts/python/stats_headless_canfar.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python - -# Name: stats_headless_canfar.py - -# Caution: Does not show all running or pending -# headless jobs, for some reason. - -import sys -from skaha.session import Session - - -def main(argv=None): - - print( - "# Depreciated, does not show pending jobs; use stats_jobs_canfar.sh", - file=sys.stderr, - ) - - session = Session() - - n_headless = session.stats()["instances"]["headless"] - - print(n_headless) - - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/scripts/sh/canfar_download_results.bash b/scripts/sh/canfar_download_results.bash deleted file mode 100755 index 991af515a..000000000 --- a/scripts/sh/canfar_download_results.bash +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash - -# Name: canfar_download_results.bash -# Description: Download ShapePipe results (.tgz files) -# from canfar with vos -# Author: Martin Kilbinger -# Date: v1.0 05/2020 -# v1.1 01/2021 - -# Command line - -## Default parameters -INPUT_VOS="cosmostat/kilbinger/results" -VERBOSE=0 -psf="mccd" -only_mask=0 - - -usage="Usage: $(basename "$0") [OPTIONS] -\n\nOptions:\n - -h\tthis message\n - -i, --input_IDs ID_FILE\n - \tASCII file with tile IDs to download, default:\n - \tdownload all available IDs\n - --input_vos PATH\n - \tinput path on vos:cfis, default='$INPUT_VOS'\n - -p, --psf MODEL\n - \tPSF model, one in ['psfex'|'mccd'], default='$psf'\n - -m\tonly mask files\n - -v\tverbose output\n -" - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -i|--input_IDs) - IDs=(`cat $2`) - echo "Downloading ${#IDs[@]} ID(s)" - shift - ;; - --input_vos) - INPUT_VOS="$2" - shift - ;; - -p|--psf) - psf="$2" - shift - ;; - -m) - only_mask=1 - ;; - -v) - VERBOSE=1 - ;; - *) - echo "Invalid command line argument '$1'" - echo -ne $usage - exit 1 - ;; - esac - shift -done - -## Check options -if [ "$psf" != "psfex" ] && [ "$psf" != "mccd" ]; then - echo "PSF (option -p) needs to be 'psf' or 'mccd'" - exit 2 -fi - -## Paths -remote="vos:cfis/$INPUT_VOS" -local="." - -if [ $only_mask == 1 ]; then - NAMES=("pipeline_flag") -else - NAMES=( - "final_cat" - "logs" - "setools_mask" - "setools_stat" - "setools_plot" - "pipeline_flag" - ) - - if [ $psf == "psfex" ]; then - NAMES+=( - "psfex_interp_exp" - ) - else - NAMES+=( - "mccd_fit_val_runner" - ) - fi -fi - -if [ $VERBOSE == 1 ]; then - vflag="-v" -else - vflag="" -fi -export VCP="vcp $vflag" - - -### Start ### - -# Download files -for name in ${NAMES[@]}; do - if [ ${#IDs[@]} == 0 ]; then - cmd="$VCP $remote/$name*.tgz $local" - $cmd - else - for ID in ${IDs[@]}; do - cmd="$VCP $remote/${name}_$ID.tgz $local" - $cmd - done - fi -done - -# Check number of files -for name in ${NAMES[@]}; do - n_downl=(`ls -l $local/${name}_*.tgz | wc`) - echo "$n_downl '$name' result files downloaded from $remote" -done diff --git a/scripts/sh/canfar_submit_selection.sh b/scripts/sh/canfar_submit_selection.sh deleted file mode 100755 index 3698f0722..000000000 --- a/scripts/sh/canfar_submit_selection.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash - -# Name: canfar_submit_selection.sh -# Author: Martin Kilbinger, martin.kilbinger@cea.fr -# Date: 2020 -# Description: Submits jobs to canfar, each jobs processes -# one tile - - -# Variables - -# Job file for one tile -SP_ROOT=$HOME/shapepipe -sp_job="$SP_ROOT/scripts/sh/job_sp.bash" - - -# Functions - -function add_queue() { - local job_file=$1 - - echo "queue" >> $job_file - echo >> $job_file -} - -function create_job_file() { - local tile_ID=$1 - local job_file=$2 - - echo "executable = $sp_job" > $job_file - echo >> $job_file - - add_to_job_file $tile_ID $job_file - - finalize_job_file $job_file - add_queue $job_file -} - -function add_to_job_file () { - local tile_ID=$1 - local job_file=$2 - - echo "arguments = $tile_ID" >> $job_file - echo "output = log_canfar_sp_$tile_ID.out" >> $job_file - echo "error = log_canfar_sp_$tile_ID.err" >> $job_file - echo "log = log_canfar_sp_$tile_ID.log" >> $job_file -} - -function finalize_job_file() { - local job_file=$1 - - echo "request_cpus = 8" >> $job_file - - # Cannot be larger than VM available RAM - echo "request_disk = 100G" >> $job_file - - echo >> $job_file -} - -function usage () { - ex=$1 - - echo "Usage: canfar_submit_selection.sh ID_path image [-h] [-n]" - echo "Options:" - echo " ID_path ascii file with tile IDs" - echo " image VM image name, newest is ShapePipe2-mk-20200820" - echo " -n dry run" - echo " -h this message" - - exit $ex -} - -if [ "${!#}" == "-h" ]; then - usage 0 -fi -if [ "$#" -lt 2 ] ; then - usage 1 -fi - -if [ "${!#}" == "-n" ]; then - dry_run=1 - dry_str=" (dry run)" -else - dry_run=0 - dry_str="" -fi - - -## Start - -tile_ID_list=$1 -image=$2 - -# Create job file -job_file="job_tile.sh" -echo "executable = $sp_job" > $job_file -echo >> $job_file - -echo "output = log_canfar_sp_\$(arguments).out" >> $job_file -echo "error = log_canfar_sp_\$(arguments).err" >> $job_file -echo "log = log_canfar_sp_\$(arguments).log" >> $job_file -echo >> $job_file -finalize_job_file $job_file -echo >> $job_file -echo "queue arguments from (" >> $job_file - -# Go through tile IDs and add job -while read -r tile_ID; do - echo "$tile_ID" >> $job_file -done < $tile_ID_list - echo ")" >> $job_file - -# Submit -cmd="canfar_submit $job_file $image c8-30gb-186" -echo "Running $cmd$dry_str" -if [ $dry_run == 0 ]; then - $cmd - condor_q -fi diff --git a/scripts/sh/post_proc_sp.bash b/scripts/sh/post_proc_sp.bash deleted file mode 100755 index 930d5b79d..000000000 --- a/scripts/sh/post_proc_sp.bash +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash - -# Name: post_proc_sp.bash -# Description: Post-process downloaded canfar results and -# creates final merge catalog. -# Author: Martin Kilbinger -# Date: 06/2020 -# Package: shapepipe - -# The shapepipe python virtual environment needs to -# be active to run this script. - - -# Command line arguments - -## Default values -psf='mccd' - -## Help string -usage="Usage: $(basename "$0") [OPTIONS] -\n\nOptions:\n - -h\tthis message\n - -p, --psf MODEL\n - \tPSF model, one in ['psfex'|'mccd'], default='$psf'\n -" - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -p|--psf) - psf="$2" - shift - ;; - *) - echo -ne $usage - exit 1 - ;; - esac - shift -done - -## Check options -if [ "$psf" != "psfex" ] && [ "$psf" != "mccd" ]; then - echo "PSF (option -p) needs to be 'psfex' or 'mccd'" - exit 2 -fi - -# Paths -export SP_RUN=. -SP_BASE=$HOME/astro/repositories/github/shapepipe -SP_CONFIG=$SP_BASE/example/cfis - - -# To download results from canfar, use -# -# canfar_download_results.sh -# -# On candide this needs to be done on -# the login node. - -# To Un-tar all .tgz results files, use -# -# $SP_BASE/scripts/sh/untar_results.sh - - -# PSF - -## Collect all psfinterp results -combine_runs -p $psf -t psf - -## Merge all psfinterp results and compute PSF residuals -shapepipe_run -c $SP_CONFIG/config_MsPl_$psf.ini - - -# Galaxies - -## Prepare output directory with links to all 'final_cat' result files -combine_runs - -## Merge final output files to single mother catalog -input_final=output/run_sp_combined/make_catalog_runner/output -merge_final_cat -i $input_final -p $SP_CONFIG/final_cat.param -v diff --git a/uv.lock b/uv.lock index ca985ee4c..5c97988f1 100644 --- a/uv.lock +++ b/uv.lock @@ -3516,7 +3516,6 @@ dependencies = [ { name = "python-pysap", marker = "sys_platform == 'linux'" }, { name = "reproject", marker = "sys_platform == 'linux'" }, { name = "sf-tools", marker = "sys_platform == 'linux'" }, - { name = "skaha", marker = "sys_platform == 'linux'" }, { name = "skyproj", marker = "sys_platform == 'linux'" }, { name = "sqlitedict", marker = "sys_platform == 'linux'" }, { name = "termcolor", marker = "sys_platform == 'linux'" }, @@ -3613,7 +3612,6 @@ requires-dist = [ { name = "ruff", marker = "extra == 'lint'" }, { name = "sf-tools", specifier = ">=2.0.4" }, { name = "shapepipe", extras = ["doc", "jupyter", "lint", "plot", "release", "test", "fitsio"], marker = "extra == 'dev'" }, - { name = "skaha", specifier = ">=1.7" }, { name = "skyproj" }, { name = "skyproj", marker = "extra == 'plot'" }, { name = "snakemake", marker = "extra == 'jupyter'", specifier = ">=9.22.0" }, @@ -3646,24 +3644,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "skaha" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "defusedxml", marker = "sys_platform == 'linux'" }, - { name = "httpx", marker = "sys_platform == 'linux'" }, - { name = "pydantic", marker = "sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'linux'" }, - { name = "toml", marker = "sys_platform == 'linux'" }, - { name = "typer", marker = "sys_platform == 'linux'" }, - { name = "vos", marker = "sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/62/a733508ff88200c41459f5cdb72a797eb8795c833b4e9d51d31b5efe4073/skaha-1.7.0.tar.gz", hash = "sha256:ef9d69e7a4da8653cdbde3e62f18caef137130064ccb6fabed57c7c6f8bf8ef9", size = 54414, upload-time = "2025-05-28T21:17:03.631Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/d3/a3c1d569ae714ed9985adccefdfe2d6dd9f8ad7a297bd8fa4c7ed30c587b/skaha-1.7.0-py3-none-any.whl", hash = "sha256:68b0d3c925b98bf145c5f695237474fe1cea07cf9b69b4a9bae9910375dfa01a", size = 40795, upload-time = "2025-05-28T21:17:02.296Z" }, -] - [[package]] name = "skyproj" version = "2.5.0" From 29e82d4d35489c11be877e8a4d408044814687b0 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:31:40 +0200 Subject: [PATCH 23/36] docs: retire the canfar-VM retrieval and post-processing pages vos_retrieve.md declared itself obsolete on its first line and was an orphan (never in toc.rst), so deleting it also clears a standing Sphinx warning. post_processing.md documented the pre-v1.4 canfar-VM chain and named create_sample_results, which does not exist; PSF validation and the rho-statistics live in sp_validation now. random_cat.md keeps the module, which is current, and stops calling tools that no longer ship: the mask-staging and mask-download steps become a statement of what random_cat_runner needs on input, and the joint-catalogue step says plainly that merge_final_cat (retired at 2ef07e45) has no successor and the concatenation is a hand step. The header-download step stays: config_get_tiles_vos_headers.ini is live. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- docs/source/post_processing.md | 81 ---------------------------- docs/source/random_cat.md | 97 +++++++++------------------------- docs/source/toc.rst | 1 - docs/source/vos_retrieve.md | 51 ------------------ 4 files changed, 26 insertions(+), 204 deletions(-) delete mode 100644 docs/source/post_processing.md delete mode 100644 docs/source/vos_retrieve.md diff --git a/docs/source/post_processing.md b/docs/source/post_processing.md deleted file mode 100644 index e8a3768f4..000000000 --- a/docs/source/post_processing.md +++ /dev/null @@ -1,81 +0,0 @@ -# Post-processing - -This page shows all required steps of post-processing the results from one or -more `ShapePipe` runs. Post-processing combines various individual `ShapePipe` -output files, and creates joint results, for example combining individual tile -catalogues into a large sky area. The output of post-processing is a joint _shape -catalogue_, containing all required information to create a calibrated shear -catalogue via _metacalibration_), a joint star catalogue, and PSF diagnostic plots. - - - ---- - -If main ShapePipe processing happened at the old canfar VM system (e.g. CFIS v0 and v1), go -[here](vos_retrieve.md) for details how to retrieve the ShapePipe output files. - ---- - -```{note} -This page documents the **legacy** post-processing used for pre-v1.4 runs on the -canfar VM system. PSF validation and the scale-dependent diagnostics -(ρ-statistics) now live in -[`sp_validation`](https://github.com/CosmoStat/sp_validation) rather than in -ShapePipe; the steps below are retained for reference and for reproducing older -runs. -``` - -The following steps were used for pre-v1.4 runs performed on the canfar VM system. - -1. Optional: Split output into sub-samples - - An optional intermediate step is to create directories for sub-samples, for example one directory - for each patch on the sky. This will create symbolic links to the results `.tgz` files downloaded in - the previous step. For example, to create the subdir `tiles_W3` with links to result files to `all` for - those tiles contained in the list `tiles_W3.txt`, do: - ```bash - create_sample_results --input_IDs tiles_W3.txt -i . all -o tiles_W3 -v - ``` - The following steps will then be done in the directory `tiles_W3`. - -2. Run PSF diagnostics, create merged catalogue - - Type - ```bash - post_proc_sp -p PSF - ``` - to automatically perform a number of post-processing steps. Choose the PSF model with the option - `-p psfex|mccd`. In detail, these are (and can also be done individually - by hand): - - 1. Analyse psf validation files - - ```bash - combine_runs -t psf -p PSF - ``` - with options as for `post_proc_sp`. - This script creates a new combined psf run in the ShapePipe `output` directory, by identifying all psf validation files - and creating symbolic links. The run log file is updated. - - 3. Merge the individual PSF validation files into one catalogue, and create - plots of the PSF and its residuals in the focal plane as a diagnostic of - the overall PSF model: - ```bash - shapepipe_run -c /path/to/shapepipe/example/cfis/config_MsPl_PSF.ini - ``` - The scale-dependent PSF diagnostics that propagate to the shear correlation - function (the ρ-statistics, {cite:p}`rowe:10`, {cite:p}`jarvis:16`) are no - longer computed here — they have moved to - [`sp_validation`](https://github.com/CosmoStat/sp_validation). - - 4. Merge final output files - - Create a single main shape catalogue: - ```bash - merge_final_cat -i -p -v - ``` - Choose as input directory `input_dir` the `make_cat` output of the runs - being combined. A default parameter file `` is - `/path/to/shapepipe/example/cfis/final_cat.param`. - On success, the file `./final_cat.npy` is created. Depending on the number of - input tiles, this file can be several tens of Gb large. diff --git a/docs/source/random_cat.md b/docs/source/random_cat.md index a930d40ed..031a3b22b 100644 --- a/docs/source/random_cat.md +++ b/docs/source/random_cat.md @@ -3,28 +3,25 @@ This section describes how to create tile-based random catalogues and healpix masks, and combined randoms and masks for a selection of tiles. -The masked regions are obtained on input from ShapePipe pixel mask ("pipeline flag") -files. +The masked regions are obtained on input from ShapePipe pixel mask ("pipeline +flag") files. ```{note} -Parts of this procedure use the legacy canfar-VM / `vos` retrieval workflow (see -[VOSpace retrieval](vos_retrieve.md)) and the obsolete `prepare_tiles_for_final` -helper, which is no longer shipped. The `random_cat` module itself is current; -the input-staging and joint-mask steps now overlap with -[`sp_validation`](https://github.com/CosmoStat/sp_validation). The steps are -retained for reference. +The `random_cat` module itself is current. The staging and joint-mask steps +below predate the Snakemake workflow and are retained for reference; the +helpers they used to call (`prepare_tiles_for_final`, `merge_final_cat`, +`canfar_avail_results`, `canfar_download_results`) are no longer shipped, and +where a step named one it now says what the step has to achieve instead. ``` ## Set up ### ID file and shell variables -First, if if does not exist already, create the file ``tile_numbers.txt`` containing a list of tile IDs, -one per line. This is the same format as the input file to ``get_images_runner``. -For example, link to a patch ID list, -```bash -ln -s tiles_PX.txt tile_numbers.txt -``` +First, if it does not exist already, create the file ``tile_numbers.txt``: a +tile list, one ID per line. This is the same format as the input file to +``get_images_runner``. + Next, set the run and config paths, ```bash export SP_RUN=. @@ -33,56 +30,23 @@ export SP_CONFIG=/path/to/config-files ### Get images or image headers -We need to footprint of the image tiles. If they have been downloaded for a ``ShapePipe`` run, -check that they are accessible as last run of the ``get_images_runner`` module. +We need the footprint of the image tiles. If they have been downloaded for a +``ShapePipe`` run, check that they are accessible as last run of the +``get_images_runner`` module. If not, we can just download the headers to gain significant download time. ```bash shapepipe_run -c $SP_CONFIG/config_get_tiles_vos_headers.ini ``` -### Check pixel mask files - -Make sure that all pixel mask files are present. If they have been downloaded from ``vos`` as ``.tgz`` files, -type -```bash -canfar_avail_results -i tile_numbers.txt --input_path . -v -m -o missing_mask.txt -``` -In case of missing mask files, check whether they are present in the ``vos`` remote directory, -```bash -canfar_avail_results -i tile_numbers.txt --input_path vos:cfis/vos-path/to/results -v -m -``` -If missing on ``vos``, process those tiles. If processing only up the the mask is necessary, -the following steps can be carried out, -```bash -job_sp -j 7 TILE_ID -job_sp -j 128 TILE_ID -``` -The first command processes the tile up to the mask; the second line uploads the mask files -to ``vos``. +### Collect the pixel mask files -Now, download the missing masks with -```bash -canfar_download_results -i missing_mask.txt --input_vos vos-path/to/results -m -v -``` -Untar .tgz files if required, -```bash -while read p; do tar xvf pipeline_flag_$p.tgz; done Date: Sat, 5 Sep 2026 19:33:00 +0200 Subject: [PATCH 24/36] refactor: retire the CANFAR headless run layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Gen 1 generation — CANFAR headless sessions driving the `tile_runs/` / `exp_runs/` layout — is what the Snakemake workflow replaces rule for rule, and much of it was already broken: - curl_canfar_local.sh called call_curl with 14 args against a 10-parameter function, and polled `stats_jobs_canfar.sh -w all`, an option that script never parsed. - init_run_exclusive_canfar.sh was the run layout in one file: the `${kind}_runs/$ID` roots, the symlink pull-in, the bit codes. The rules in prepare/exposure/tile.smk plus the Snakefile's unit_pre and sp_shell carry all of it now. - canfar_async_job.sh was already unreachable: run_async always selected async_bulk. - run_scratch_local.sh was orphaned and broken (it guards $exclusive after unconditionally assigning $ID). - combine_runs.bash built a post-hoc symlink farm; tile_make_cat and exp_persist publish to products_dir instead. - remove_duplicates_tiles.sh cleaned up duplicate timestamped run dirs, which one-rule-one-store makes structurally impossible. - stats_jobs_canfar.sh parsed skaha JSON by grep line offsets; canfar_monitor -q does it through the API. - distribute_tiles.py and link_to_exp_for_tile.py chunked and globbed what run_index.sqlite and build_forest.py now index; the latter was already dead (its only caller commented it out for P9). - clear_ngmix_prev.py had zero references anywhere. - get_number_objects.py's count floors live in completeness.py. canfar_submit.py goes with them: every mode it can select execs one of these three scripts (async_single → canfar_async_job.sh, async_bulk → distribute_tiles.py, sync → init_run_exclusive_canfar.sh), so the head of the chain has nothing left to submit. Its console entry point and bin/ shim go too. canfar_monitor and canfar_log_monitor stay — they are layout-independent session management, and the substitute for the stats script deleted here. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- CLAUDE.md | 2 +- bin/canfar_submit_job.py | 3 - pyproject.toml | 1 - scripts/python/clear_ngmix_prev.py | 155 ------ scripts/python/distribute_tiles.py | 289 ----------- scripts/python/get_number_objects.py | 243 --------- scripts/python/link_to_exp_for_tile.py | 467 ----------------- scripts/sh/canfar_async_job.sh | 40 -- scripts/sh/combine_runs.bash | 224 --------- scripts/sh/curl_canfar_local.sh | 303 ----------- scripts/sh/init_run_exclusive_canfar.sh | 642 ------------------------ scripts/sh/job_sp_canfar.bash | 610 ---------------------- scripts/sh/remove_duplicates_tiles.sh | 55 -- scripts/sh/run_scratch_local.sh | 178 ------- scripts/sh/stats_jobs_canfar.sh | 119 ----- src/shapepipe/canfar/canfar_submit.py | 576 --------------------- src/shapepipe/canfar_run.py | 20 +- workflow/README.md | 9 +- 18 files changed, 7 insertions(+), 3929 deletions(-) delete mode 100755 bin/canfar_submit_job.py delete mode 100644 scripts/python/clear_ngmix_prev.py delete mode 100755 scripts/python/distribute_tiles.py delete mode 100755 scripts/python/get_number_objects.py delete mode 100755 scripts/python/link_to_exp_for_tile.py delete mode 100755 scripts/sh/canfar_async_job.sh delete mode 100755 scripts/sh/combine_runs.bash delete mode 100755 scripts/sh/curl_canfar_local.sh delete mode 100755 scripts/sh/init_run_exclusive_canfar.sh delete mode 100755 scripts/sh/job_sp_canfar.bash delete mode 100755 scripts/sh/remove_duplicates_tiles.sh delete mode 100755 scripts/sh/run_scratch_local.sh delete mode 100755 scripts/sh/stats_jobs_canfar.sh delete mode 100644 src/shapepipe/canfar/canfar_submit.py diff --git a/CLAUDE.md b/CLAUDE.md index eebbc386e..fba149932 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,7 +80,7 @@ Full detail: `docs/source/installation.md` and `docs/source/container.md`. - `src/shapepipe/` — the package (src-layout). `modules/` holds the pipeline modules and their `*_runner.py` wrappers; `pipeline/` is execution and file - I/O; `utilities/`; `canfar/` is CANFAR/cluster job orchestration. Console entry + I/O; `utilities/`; `canfar/` monitors CANFAR sessions. Console entry points (`shapepipe_run`, `canfar_*`, `plot_coverage_map`) are defined under `[project.scripts]`. - `tests/` — the whole test suite, one discovery root: `module/` (per-module diff --git a/bin/canfar_submit_job.py b/bin/canfar_submit_job.py deleted file mode 100755 index 202e9d274..000000000 --- a/bin/canfar_submit_job.py +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python -from shapepipe.canfar_run import run_job -run_job() diff --git a/pyproject.toml b/pyproject.toml index f845a2621..3d468ee89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,7 +83,6 @@ dev = ["shapepipe[doc,jupyter,lint,plot,release,test,fitsio]"] [project.scripts] shapepipe_run = "shapepipe.shapepipe_run:main" -canfar_submit_job = "shapepipe.canfar_run:run_job" canfar_monitor = "shapepipe.canfar_run:run_log" canfar_monitor_log = "shapepipe.canfar_run:run_monitor_log" plot_coverage_map = "shapepipe.coverage_run:run_plot_coverage" diff --git a/scripts/python/clear_ngmix_prev.py b/scripts/python/clear_ngmix_prev.py deleted file mode 100644 index a1a7e802c..000000000 --- a/scripts/python/clear_ngmix_prev.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -""" -Clean up previous ngmix run directories for completed tiles. -Removes *_prev directories when the current run has finished. -""" - -import argparse -import os -import re -import shutil -import glob -from pathlib import Path -from astropy.io import fits - - -def get_fits_info(fits_file): - """Get number of objects (NAXIS2 in HDU 1) and number of HDUs.""" - try: - with fits.open(fits_file) as hdul: - n_hdus = len(hdul) - # Get NAXIS2 from HDU 1 (second HDU, index 1) - if len(hdul) > 1: - n_objects = hdul[1].header.get("NAXIS2", "N/A") - else: - n_objects = "N/A (no HDU 1)" - return n_objects, n_hdus - except Exception as e: - return f"Error: {e}", "N/A" - - -def check_finished(log_file): - """Check if 'finished' appears in the log file.""" - try: - with open(log_file, "r") as f: - content = f.read() - return "finished" in content - except Exception as e: - print(f"Warning: Could not read {log_file}: {e}") - return False - - -def main(): - parser = argparse.ArgumentParser( - description="Clean up previous ngmix run directories for completed tiles." - ) - parser.add_argument( - "-n", "--dry-run", - action="store_true", - help="Show what would be removed without actually removing files" - ) - args = parser.parse_args() - - if args.dry_run: - print("dry run mode: No files will be removed\n") - - base_dir = Path("tile_runs") - - # Pattern to match tile IDs like 123.456 - tile_id_pattern = re.compile(r"^\d+\.\d+$") - - # Find all tile ID directories - if not base_dir.exists(): - print(f"Error: {base_dir} does not exist") - return - - tile_dirs = [ - d - for d in base_dir.iterdir() - if d.is_dir() and tile_id_pattern.match(d.name) - ] - - print(f"Found {len(tile_dirs)} tile directories") - - removed_count = 0 - finished_count = 0 - - for tile_dir in sorted(tile_dirs): - tile_id = tile_dir.name - - # Construct path to ngmix_runner directory - ngmix_dir = ( - tile_dir / "output" / "run_sp_tile_ngmix_Ng1u" / "ngmix_runner" - ) - - if not ngmix_dir.exists(): - continue - - # Check for process-*.log files - log_pattern = str(ngmix_dir / "logs" / "process-*.log") - log_files = glob.glob(log_pattern) - - if not log_files: - continue - - # Check if any log file contains "finished" - is_finished = any(check_finished(log_file) for log_file in log_files) - - if is_finished: - finished_count += 1 - - # Path to the _prev directory to remove - prev_dir = tile_dir / "output" / "run_sp_tile_ngmix_Ng1u_prev" - - if prev_dir.exists(): - - # File info of new - ngmix_out_dir = ngmix_dir / "output" - fits_files = list(ngmix_out_dir.glob("ngmix-*.fits")) - if fits_files: - for fits_file in fits_files: - n_objects, n_hdus = get_fits_info(fits_file) - else: - n_objects, n_hdus = (-1, -1) - - # File info of prev - prev_out_dir = prev_dir / "output" - fits_files = list(prev_out_dir.glob("ngmix-*.fits")) - if fits_files: - for fits_file in fits_files: - n_objects_prev, n_hdus_prev = get_fits_info(fits_file) - else: - n_objects_prev, n_hdus_prev = (-1, -1) - - action = "Would remove" if args.dry_run else "Removing" - print( - f"Tile {tile_id}: {action} {prev_dir} (new: {n_objects}" - + f" {n_hdus}) (prev: {n_objects_prev} {n_hdus_prev})" - ) - - if n_objects < n_objects_prev: - print(f"Warning: {tile_id}") - - if not args.dry_run: - try: - shutil.rmtree(prev_dir) - removed_count += 1 - except Exception as e: - print(f"Error removing {prev_dir}: {e}") - else: - removed_count += 1 - else: - pass - else: - print(f"Tile {tile_id}: Not finished") - - print(f"\nSummary:") - print(f" Tiles with finished runs: {finished_count}") - if args.dry_run: - print(f" Previous directories that would be removed: {removed_count}") - else: - print(f" Previous directories removed: {removed_count}") - - -if __name__ == "__main__": - main() diff --git a/scripts/python/distribute_tiles.py b/scripts/python/distribute_tiles.py deleted file mode 100755 index 338286002..000000000 --- a/scripts/python/distribute_tiles.py +++ /dev/null @@ -1,289 +0,0 @@ -#!/usr/bin/env python -""" -distribute_tiles.py - -Uses canfar.helpers.distributed.chunk to automatically distribute tiles -across replicas, then processes each tile assigned to this replica. -""" - -import os -import sys -import subprocess -from multiprocessing import Pool -import fcntl -from canfar.helpers.distributed import chunk - - -def parse_arguments(args): - """ - Extract the file IDs from command line arguments. - - Args: - args (list of str): Original command line arguments (e.g., sys.argv[1:]) - - Returns: - str or None: The value passed with -f, or None if not present - """ - # Default values - parsed = {"dry_run": 0, "parallel_jobs": 1} - - i = 0 - while i < len(args): - if args[i] == '-f' and i + 1 < len(args): - parsed['file_ids'] = args[i + 1] - i += 2 - elif args[i] == '--batch_num' and i + 1 < len(args): - parsed['batch_num'] = int(args[i + 1]) - i += 2 - elif args[i] == '--batch_tot' and i + 1 < len(args): - parsed['batch_tot'] = int(args[i + 1]) - i += 2 - elif args[i] == '--batch_size' and i + 1 < len(args): - parsed['batch_size'] = int(args[i + 1]) - i += 2 - elif args[i] in ('-n', '--dry_run') and i + 1 < len(args): - parsed['dry_run'] = int(args[i + 1]) - i += 2 - elif args[i] == '--parallel_jobs' and i + 1 < len(args): - parsed['parallel_jobs'] = int(args[i + 1]) - i += 2 - elif args[i] == "--debug_out" and i + 1 < len(args): - parsed["debug_out"] = args[i + 1] - i += 2 - else: - i += 1 - - return parsed - - -def get_my_tiles(all_tiles, batch_num, batch_tot, batch_size): - """ - Calculate which tiles this replica should process based on global position. - Args: - all_tiles: List of all tile IDs - batch_num: Current batch number (1-indexed) - batch_tot: Total number of batches - batch_size: Number of replicas per batch - Returns: - list: Tiles assigned to this replica - - """ - # Get local replica info from environment - local_replica_id = int(os.environ.get('REPLICA_ID', 1)) - - start_idx = (batch_num - 1) * batch_size - end_idx = min(batch_num * batch_size, len(all_tiles)) - - batch_tiles = all_tiles[start_idx:end_idx] - print(f"Batch {batch_num}/{batch_tot}, local replica {local_replica_id}/{batch_size}") - print(f"Batch processes tiles {start_idx + 1} to {end_idx} ({len(batch_tiles)} tiles)") - - # Use chunk() to distribute this batch's tiles among local replicas - # chunk() will use REPLICA_ID and REPLICA_COUNT automatically - my_tiles = list(chunk(batch_tiles)) - - return my_tiles - -def get_tile_list(file_ids): - """Read all tile IDs from file. - - Args: - file_ids: Path to file containing tile IDs - - Returns: - list: List of tile ID strings - """ - with open(file_ids, 'r') as f: - tiles = [line.strip() for line in f if line.strip()] - return tiles - - -def build_process_command(tile_id, original_args): - """ - Build command for a single tile by replacing -f with -e . - - Args: - tile_id (str): Tile ID to process - original_args (list of str): Original CLI arguments - - Returns: - list of str: Command ready for subprocess - """ - cmd = [f"{os.environ['HOME']}/shapepipe/scripts/sh/init_run_exclusive_canfar.sh"] - - # Transform arguments: replace -f with -e , - # skip batch arguments, parallel_jobs, and dry_run if not 0, 1 - i = 0 - while i < len(original_args): - if original_args[i] == '-f' and i + 1 < len(original_args): - cmd.extend(['-e', tile_id]) - i += 2 - elif original_args[i] in ('--batch_num', '--batch_tot', '--batch_size', '--parallel_jobs'): - i += 2 - elif original_args[i] in ('-n', '--dry_run'): - if i + 1 < len(original_args) and original_args[i + 1] in ("0", "1"): - cmd.extend(["-n", original_args[i + 1]]) - i += 2 - else: - cmd.append(original_args[i]) - i += 1 - - return cmd - - -def process_single_tile(args_tuple): - """ - Process a single tile (designed for multiprocessing). - - Args: - args_tuple: Tuple of (tile_id, tile_num, total_tiles, original_args, dry_run, msg_batch) - - Returns: - tuple: (tile_id, success, error_message) - """ - tile_id, tile_num, total_tiles, original_args, dry_run, msg_batch = args_tuple - - print(f"{'='*5} Processing {msg_batch}tile num/total={tile_num}/{total_tiles}: ID={tile_id} {'='*5}") - - # Build command - cmd = build_process_command(tile_id, original_args) - print(f"Command: {' '.join(cmd)}") - - # Execute command - try: - if dry_run != 2: - result = subprocess.run(cmd, check=True, capture_output=True, text=True) - print(f"✓ Successfully processed tile {tile_id}") - return (tile_id, True, None) - else: - print(f"dry_run=2") - return (tile_id, True, None) - except subprocess.CalledProcessError as e: - error_msg = f"Failed to process tile {tile_id}: {e}" - print(f"✗ {error_msg}", file=sys.stderr) - return (tile_id, False, error_msg) - except Exception as e: - error_msg = f"Unexpected error processing tile {tile_id}: {e}" - print(f"✗ {error_msg}", file=sys.stderr) - return (tile_id, False, error_msg) - - -def print_debug(pat, tile_list, out_path, verbose=False): - - local_replica_id = int(os.environ.get('REPLICA_ID', 1)) - - with open(out_path, "a") as f: - # Exclusive lock for save parallel use - fcntl.flock(f.fileno(), fcntl.LOCK_EX) - try: - # Build output string - output = f"{pat} distribute_tiles REPLICA_ID={local_replica_id}, tiles=" - output += " ".join(tile_list) + "\n" - - # Write to file - f.write(output) - - # Optionally write to stdout - if verbose: - print(output, end="") - finally: - # Unlock - fcntl.flock(f.fileno(), fcntl.LOCK_UN) - - -def main(): - - # Debug file line pattern - pat = "- " - - # Parse arguments - args = parse_arguments(sys.argv[1:]) - - if not "file_ids" in args: - print("Error: -f must be provided", file=sys.stderr) - sys.exit(1) - else: - file_ids = args["file_ids"] - - # Read all tiles - print(f"Reading tile list from {file_ids}") - all_tiles = get_tile_list(file_ids) - print(f"Total tiles in file: {len(all_tiles)}") - - print( - "REPLICA_ID, REPLICA_COUNT=", - os.environ.get('REPLICA_ID'), - os.environ.get('REPLICA_COUNT') - ) - - - # Check if we're in multi-batch mode - if "batch_num" in args and "batch_tot" in args and "batch_size" in args: - # Multi-batch mode: calculate global distribution - my_tile_list = get_my_tiles( - all_tiles, - args['batch_num'], - args['batch_tot'], - args['batch_size'] - ) - msg_batch = f"batch {args['batch_num']}/{args['batch_tot']} " - - else: - # Use chunk() to get tiles for this replica - print("Using chunk() to determine tiles for this replica...") - my_tile_list = list(chunk(all_tiles)) - msg_batch = "" - - if "debug_out" in args: - print_debug(f"{pat}{msg_batch}", my_tile_list, args["debug_out"], verbose=True) - - print(f"This replica assigned {len(my_tile_list)} tiles") - print(f"Parallel jobs: {args['parallel_jobs']}") - - if len(my_tile_list) > 0: - print(f"First tile: {my_tile_list[0]}") - if len(my_tile_list) > 1: - print(f"Last tile: {my_tile_list[-1]}") - else: - print("Only one tile") - - # Process each tile assigned to this replica - success_count = 0 - failure_count = 0 - - # Prepare arguments for parallel processing - process_args = [ - (tile_id, i, len(my_tile_list), sys.argv[1:], args["dry_run"], msg_batch) - for i, tile_id in enumerate(my_tile_list, 1) - ] - - if args['parallel_jobs'] > 1: - # Parallel processing - print(f"Processing tiles in parallel with {args['parallel_jobs']} workers") - with Pool(processes=args['parallel_jobs']) as pool: - results = pool.map(process_single_tile, process_args) - else: - # Sequential processing (original behavior) - print("Processing tiles sequentially") - results = [process_single_tile(arg) for arg in process_args] - - # Count successes and failures - for tile_id, success, error_msg in results: - if success: - success_count += 1 - else: - failure_count += 1 - - # Summary - print( - f"{'='*5} Processing completed tot/suc/fail=" - + f"{len(my_tile_list)}/{success_count}/{failure_count}" - ) - - # Exit with error if any failures - if failure_count > 0: - sys.exit(1) - - -if __name__ == '__main__': - main() diff --git a/scripts/python/get_number_objects.py b/scripts/python/get_number_objects.py deleted file mode 100755 index 98422457c..000000000 --- a/scripts/python/get_number_objects.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python - -# -*- coding: utf-8 -*- - -"""Script get_number_objects.py - -Get number of objects in a (last-run SExtractor) catalogue. - -:Author: Martin Kilbinger - -""" - -import sys -import copy -import glob - -from optparse import OptionParser -from astropy.io import fits - -from shapepipe.pipeline.run_log import get_last_dir, get_all_dirs -from shapepipe.utilities import cfis - - -class param: - """General class to store (default) variables""" - - def __init__(self, **kwds): - self.__dict__.update(kwds) - - def print(self, **kwds): - print(self.__dict__) - - def var_list(self, **kwds): - return vars(self) - - -def params_default(): - """Set default parameter values. - - Returns - ------- - class param - parameter values - - """ - p_def = param( - input_path=".", - input_name_base="final_cat", - hdu_num=1, - ) - - return p_def - - -def parse_options(p_def): - """Parse command line options. - - Parameters - ---------- - p_def: class param - parameter values - - Returns - ------- - list - command line options - command line str - - """ - usage = "%prog [OPTIONS]" - parser = OptionParser(usage=usage) - - # IO - parser.add_option( - "-i", - "--input_path", - dest="input_path", - type="string", - default=p_def.input_path, - help=f"input path, default='{p_def.input_path}'", - ) - parser.add_option( - "-n", - "--input_name_base", - dest="input_name_base", - type="string", - default=p_def.input_name_base, - help=f"input name base, default='{p_def.input_name_base}'", - ) - parser.add_option( - "-l", - "--list_tile_ID_path", - dest="tile_ID_list_path", - type="string", - default=None, - help=f"tile ID list, default: Use all data in input files", - ) - - # Control - parser.add_option( - "-p", - "--param_path", - dest="param_path", - type="string", - default=None, - help="parameter file path, default=None", - ) - - parser.add_option( - "", - "--hdu_num", - dest="hdu_num", - type="int", - default=p_def.hdu_num, - help=f"input HDU number, default='{p_def.hdu_num}'", - ) - - parser.add_option( - "-v", - "--verbose", - dest="verbose", - action="store_true", - help="verbose output", - ) - - options, args = parser.parse_args() - - return options, args - - -def check_options(options): - """Check command line options. - - Parameters - ---------- - options: tuple - Command line options - - Returns - ------- - bool - Result of option check. False if invalid option value. - - """ - return True - - -def update_param(p_def, options): - """Return default parameter, updated and complemented according to options. - - Parameters - ---------- - p_def: class param - parameter values - optiosn: tuple - command line options - - Returns - ------- - class param - updated paramter values - - """ - param = copy.copy(p_def) - - # Update keys in param according to options values - for key in vars(param): - if key in vars(options): - setattr(param, key, getattr(options, key)) - - # Add remaining keys from options to param - for key in vars(options): - if not key in vars(param): - setattr(param, key, getattr(options, key)) - - # Do extra stuff if necessary - - return param - - -def main(argv=None): - - # Set default parameters - p_def = params_default() - - # Command line options - options, args = parse_options(p_def) - - if check_options(options) is False: - return 1 - - param = update_param(p_def, options) - - # Save command line arguments to log file - f_log = cfis.log_command(argv, close_no_return=False) - - pattern = "sexcat" - run_log_file = "output/log_run_sp.txt" - - # For v1 - # module = 'sextractor_runner_run_1' - - # For v2 - module = "sextractor_runner" - - # Get all sextractor output directories - all_dir = get_all_dirs(run_log_file, module) - paths = [] - - # Find tile runs - for path in all_dir: - if "run_sp_tile_Sx" in path: - paths.append(path) - paths = sorted(paths) - - if len(paths) == 0: - # No previous tile SExtractor run found - return -1 - - # Get latest run - last_dir = paths[-1] - - # Get all output SExtractor catalogues - file_list = glob.glob(f"{last_dir}/{pattern}*.fits") - if len(file_list) == 0: - raise ValueError(f"No files {last_dir}/{pattern}*.fits found") - - # Add up number of objects over all catalogues - n_obj = 0 - hdu_no = -1 - for fpath in file_list: - hdu_list = fits.open(fpath) - header = hdu_list[-1].header - n_obj += int(header["NAXIS2"]) - - # Compute average - n_obj = int(n_obj / len(file_list)) - - print(n_obj) - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/scripts/python/link_to_exp_for_tile.py b/scripts/python/link_to_exp_for_tile.py deleted file mode 100755 index 5b2a5f0e2..000000000 --- a/scripts/python/link_to_exp_for_tile.py +++ /dev/null @@ -1,467 +0,0 @@ -#!/usr/bin/env python - -# -*- coding: utf-8 -*- - -"""Script link_to_exp_for_tile.py - -:Description: Link to exposure and PSF catalogue - for a given tile. - -:Author: Martin Kilbinger - -""" - -import os -import sys -import re -import copy - -from optparse import OptionParser - - -class param: - """General class to store (default) variables""" - - def __init__(self, **kwds): - self.__dict__.update(kwds) - - def print(self, **kwds): - print(self.__dict__) - - def var_list(self, **kwds): - return vars(self) - - -def params_default(): - """Params Default. - - Set default parameter values. - - Returns - ------- - class param - parameter values - - """ - p_def = param( - tile_base_dir=".", - exp_base_dir=".", - sp_local=0, - ) - - return p_def - - -def parse_options(p_def): - """Parse Options. - - Parse command line options. - - Parameters - ---------- - p_def: class param - parameter values - - Returns - ------- - list - command line options - command line str - - """ - usage = "%prog [OPTIONS]" - parser = OptionParser(usage=usage) - - # IO - parser.add_option( - "-i", - "--input_tile_dir", - dest="tile_base_dir", - type="string", - default=p_def.tile_base_dir, - help=f"input tile base directory, default='{p_def.tile_base_dir}'", - ) - parser.add_option( - "-t", - "--tile_ID", - dest="tile_ID", - type="string", - help=f"input tile ID", - ) - parser.add_option( - "-I", - "--input_exp_dir", - dest="exp_base_dir", - type="string", - default=p_def.exp_base_dir, - help=f"input exposure base directory, default='{p_def.exp_base_dir}'", - ) - parser.add_option( - "-s", - "--sp_local", - dest="sp_local", - type="int", - default=p_def.sp_local, - help=f"local runi of split_exposure_runner, default='{p_def.sp_local}'", - ) - parser.add_option( - "-v", - "--verbose", - dest="verbose", - action="store_true", - help="verbose output", - ) - - options, args = parser.parse_args() - - return options, args - - -def check_options(options): - """Check Options. - - Check command line options. - - Parameters - ---------- - options: tuple - Command line options - - Returns - ------- - bool - Result of option check. False if invalid option value. - - """ - return True - - -def update_param(p_def, options): - """Update Param. - - Return default parameter, updated and complemented according to options. - - Parameters - ---------- - p_def: class param - parameter values - optiosn: tuple - command line options - - Returns - ------- - class param - updated paramter values - - """ - param = copy.copy(p_def) - - # Update keys in param according to options values - for key in vars(param): - if key in vars(options): - setattr(param, key, getattr(options, key)) - - # Add remaining keys from options to param - for key in vars(options): - if not key in vars(param): - setattr(param, key, getattr(options, key)) - - # Do extra stuff if necessary - - return param - - -# TODO: move to cs_util -def matching_subdirs(base_dir, pattern): - """Matching Subdirs. - - Return all subdirectories whose name start with a given string pattern. - - Parameters - ---------- - base_dir: str - base directory - pattern: str - pattern to match beginning of subdir name - - Returns - ------- - list - matched subdirectory names - - """ - subdirs = [] - if os.path.exists(base_dir): - # Loop over directory entries - for entry in os.listdir(base_dir): - full_path = os.path.join(base_dir, entry) - # Append if match - if os.path.isdir(full_path) and entry.startswith(pattern): - subdirs.append(full_path) - else: - print(f"Warning: {base_dir} does not exist, continuing...") - - # Sort according to creation date - subdirs.sort(key=os.path.getctime) - - return subdirs - - -def get_tile_out_dir(tile_base_dir, tile_ID): - """Get Tile Out Dir. - - Return output directory path for a given tile ID. - - Parameters - ---------- - tile_base_dir: str - base directory of tile runs - tile_ID: str - tile ID - - Returns - ------- - str - path - - """ - return f"{tile_base_dir}/{tile_ID}/output" - - -def get_exp_IDs(tile_base_dir, tile_ID, verbose=False): - """Get Exp IDs. - - Return exposure IDs used for given tile. - - Parameters - ---------- - tile_base_dir: str - base directory of tile runs - tile_ID: str - tile ID - verbose: bool, optional - verbose output if ``True``; default is ``False`` - - Returns - ------- - list - exposure IDs - - """ - # Get tile output path - tile_out_dir = get_tile_out_dir(tile_base_dir, tile_ID) - - # Get subdirectories with runs "Fe" (find exposures) - pattern = "run_sp_GitFeGie" - subdirs = matching_subdirs(tile_out_dir, pattern) - - # Raise error if not exactly one run - if len(subdirs) == 0: - raise IOError( - f"No matching directory '{pattern}' in {tile_out_dir} found" - ) - if len(subdirs) != 1: - raise IOError( - f"Exactly one directory natching {pattern} in {tile_out_dir} " - + f"expected, not {len(subdirs)}" - ) - - # Replace dot with dash in tile ID - tile_ID_sp = re.sub(r"\.", "-", tile_ID) - # Get output file of find_exposure_runner for this tile ID - exp_ID_file = ( - f"{subdirs[0]}/find_exposures_runner/output/" - + f"exp_numbers-{tile_ID_sp}.txt" - ) - - exp_IDs = [] - # Read file to get exposure IDs - with open(exp_ID_file) as f_in: - for line in f_in: - name = line.strip() - # Remove any letter - ID = re.sub("[a-zA-Z]", "", name) - exp_IDs.append(ID) - - if verbose: - print("Exposures: ", exp_IDs) - return exp_IDs - - -def get_exp_single_HDU_IDs(exp_IDs, n_CPU): - """Get Exp Single HDU IDs. - - Return all single-HDU IDs for given exposures. - - Parameters - ---------- - exp_IDs: list - input exposure IDs, list of str - n_CPU: int - number of CPUs (=HDUs) of an exposure; n_CPU=40 for MegaCAM - - Returns - ------- - list - list of single-HDU exposure IDs - - """ - exp_shdu_IDs = [] - # Loop over input exposure IDs - for exp_ID in exp_IDs: - # Append int up to n_CPU to each - for idx in range(n_CPU): - ID = f"{exp_ID}-{idx}" - exp_shdu_IDs.append(ID) - - return exp_shdu_IDs - - -def get_paths(exp_base_dir, exp_shdu_IDs, pattern): - """Get Paths. - - Return (newest) subdirectory for each input single-exposure HDU ID that matches pattern. - - Parameters - ---------- - exp_base_dir: str - base directory for (single-HDU) exposure runs - exp_shdu_IDs: list - single-HDU exposure IDs; list of str - pattern: str - pattern to match beginning of subdir names - Returns - ------- - list - matching paths; list of str, one entry for each input single-exp ID - - """ - paths = [] - # Loop over single-HDU exposure IDs - for exp_shdu_ID in exp_shdu_IDs: - - # output path of runs - name = f"{exp_base_dir}/{exp_shdu_ID}/output" - path = os.path.abspath(name) - - # get matching subdirs - subdirs = matching_subdirs(path, pattern) - n_subdirs = len(subdirs) - - if n_subdirs != 1: - msg = ( - f"Exactly one directory matching {pattern} in {path} expected," - + f" not {n_subdirs}" - ) - - # If more than one found: sort by name = sort by date - subdirs = sorted(subdirs) - - # No match - if n_subdirs == 0: - continue - - # Append matching subdir; if more than one append newest - paths.append(f"{subdirs[-1]}") - - return paths - - -def create_links_paths(tile_base_dir, tile_ID, paths, verbose=False): - """Create Links Paths. - - Create links to paths. - - Parameters - ---------- - tile_base_dir: str - base directory for tile runs - tile_ID: str - tile ID - paths: list - paths; list of str - verbose: bool, optional - verbose output if ``True``; default is ``False`` - - """ - # Get tile output path - tile_out_dir = get_tile_out_dir(tile_base_dir, tile_ID) - - # Loop over paths - for path in paths: - - # Get destination = tile output dir + path tail (part of path after last slash) - head, tail = os.path.split(path) - src = path - dst = f"{tile_out_dir}/{tail}" - - if os.path.exists(dst): - - src_existing = os.readlink(dst) - if src_existing == src: - # destination already points to source: skip - if verbose: - print( - f"Warning: {src} <- {dst} already exists, no link created" - ) - continue - else: - # destination points to different source: create links with added index to distinguish - # from existing one(s) - idx = 1 - dst_orig = dst - while True: - # Find destination name with lowest appended index that does not exist - dst = f"{dst_orig}_{idx}" - if os.path.exists(dst): - idx += 1 - else: - # Found: create new link - if verbose: - print(f"link {src} <- {dst}") - os.symlink(src, dst) - break - else: - - # destination does not exist: create link - if verbose: - print(f"link {src} <- {dst}") - os.symlink(src, dst) - - -def main(argv=None): - - # Set default parameters - p_def = params_default() - - # Command line options - options, args = parse_options(p_def) - - if check_options(options) is False: - return 1 - - param = update_param(p_def, options) - - tile_base_dir = param.tile_base_dir - exp_base_dir = param.exp_base_dir - tile_ID = param.tile_ID - n_CPU = 40 - verbose = param.verbose - - exp_IDs = get_exp_IDs(tile_base_dir, tile_ID, verbose=verbose) - exp_shdu_IDs = get_exp_single_HDU_IDs(exp_IDs, n_CPU) - - # Note: psfex P3 is mostly run_sp_exp_SxSePsf - patterns = ["run_sp_exp_SxSePsfPi"] # , "run_sp_exp_Pi"] - if param.sp_local == 1: - patterns.append("run_sp_exp_Sp_shdu") - for pattern in patterns: - paths = get_paths(exp_base_dir, exp_shdu_IDs, pattern) - - create_links_paths(tile_base_dir, tile_ID, paths, verbose=verbose) - - return 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv)) diff --git a/scripts/sh/canfar_async_job.sh b/scripts/sh/canfar_async_job.sh deleted file mode 100755 index 7128e7734..000000000 --- a/scripts/sh/canfar_async_job.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -source $HOME/shapepipe/scripts/sh/functions.sh - - -# 1. Extract the line from -f file -# Save original arguments -ARGS=("$@") - -# Find -f argument and get its value; remove -f and -S -FILE_IDS="" -NEW_ARGS=() -i=0 -while [[ $i -lt $# ]]; do - arg="${ARGS[i]}" - case "$arg" in - -f) - ((i++)) - FILE_IDS="${ARGS[i]}" - ;; - -S) - ((i++)) # skip next argument (value of -S) - ;; - *) - NEW_ARGS+=("$arg") - ;; - esac - ((i++)) -done - -if [[ -z "$FILE_IDS" ]]; then - echo "Error: -f must be provided" - exit 1 -fi - -# Get the line for this replica -LINE=$(get_line_from_file -f "$FILE_IDS") - -# 2. Call init_run.sh with remaining args + -e line -$HOME/shapepipe/scripts/sh/init_run_exclusive_canfar.sh "${NEW_ARGS[@]}" -e "$LINE" diff --git a/scripts/sh/combine_runs.bash b/scripts/sh/combine_runs.bash deleted file mode 100755 index 8cbf9bbdf..000000000 --- a/scripts/sh/combine_runs.bash +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env bash - -# Name: combine_runs.bash -# Description: Create new shapepipe run directory with -# links to source files from combined existing runs -# Author: Martin Kilbinger - - -# Command line arguments - -## Default values -cat='final' -psf="mccd" - -## Help string -usage="Usage: $(basename "$0") [OPTIONS] -\n\nOptions:\n - -h\tthis message\n - -p, --psf MODEL\n - \tPSF model, allowed are 'psfex', 'mccd', 'setools', default='$psf'\n - -c, --cat TYPE\n - \tCatalogue type, allowed are 'final', 'flag_tile', 'flag_exp', \n - \t'psf', 'psf_conv', 'image', 'shdu', default='$cat'\n -" - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -c|--cat) - cat="$2" - shift - ;; - -p|--psf) - psf="$2" - shift - ;; - *) - echo -ne $usage - exit 1 - ;; - esac - shift -done - - -## Check options -if [ "$cat" != "final" ] \ - && [ "$cat" != "flag_tile" ] \ - && [ "$cat" != "tile_detection" ] \ - && [ "$cat" != "flag_exp" ] \ - && [ "$cat" != "psf" ] \ - && [ "$cat" != "psf_conv" ] \ - && [ "$cat" != "image" ] \ - && [ "$cat" != "shdu" ]; then - echo "cat (option -c) needs to be 'final', 'tile_detection', 'flag_tile', 'flag_exp', 'psf', 'psf_conv', 'shdu', or 'image'" - exit 2 -fi - -## Check options -if [ "$psf" != "psfex" ] \ - && [ "$psf" != "mccd" ] \ - && [ "$psf" != "setools" ]; then - echo "PSF (option -p) needs to be 'psfex' or 'mccd'" - exit 2 -fi - - -## Functions -function link_s () { - target=$1 - link_name=$2 - - if [ -L "$link_name" ]; then - echo "link with name $link_name already exists, skipping..." - let "n_skipped+=1" - else - echo "create link $target <- $link_name" - ln -s $target $link_name - let "n_created+=1" - fi -} - - -# Start program - -n_skipped=0 -n_created=0 - -pwd=`pwd` -out_base="output" - -# Set paths: -## run_out: target output new run directory -## run_in: source input run base directory -## module: source input module runner sub-directory -## pattern: source file pattern - -run_out="run_sp_combined_$cat" - -if [ "$cat" == "final" ]; then - - # v1 - #run_in="$pwd/$out_base/run_sp_Mc_*" - # v2 - run_in="$pwd/tile_runs/*/$out_base/run_sp_Mc_*" - - module="make_catalog_runner" - pattern="final_cat-*" - -elif [ "$cat" == "tile_detection" ]; then - - run_in="$pwd/P?/tile_runs/*/$out_base/run_sp_tile_Sx_*" - module="sextractor_runner" - pattern="sexcat-*" - -elif [ "$cat" == "flag_tile" ]; then - - # v1 - #run_in="$pwd/$out_base/run_sp_MaMa_*/mask_runner_run_1" - # v2 - run_in="$pwd/$out_base/run_sp_tile_Ma_*" - run_out="run_sp_Ma_tile" - - module="mask_runner" - pattern="pipeline_flag-*" - -elif [ "$cat" == "flag_exp" ]; then - - # v1 - #run_in="$pwd/$out_base/run_sp_MaMa_*/mask_runner_run_2" - # v2 - run_in="$pwd/$out_base/run_sp_exp_Ma_*" - run_out="run_sp_Ma_exp" - - module="mask_runner" - pattern="pipeline_flag-*" - -elif [ "$cat" == "image" ]; then - - run_in="$pwd/$out_base/run_sp_Git_*" - module="get_images_runner" - pattern="CFIS_image-*" - -elif [ "$cat" == "psf" ]; then - - #MKDEBUG TODO: add option - # v1 - #run_in="$pwf/$out_base/run_sp_exp_Pi_*" - # v2 - #run_in="$pwd/exp_runs/*/$out_base/run_sp_exp_Pi_*" - run_in="$pwd/exp_runs/*/$out_base/run_sp_exp_SxSePsfPi_*" - - pattern="validation_psf-*" - if [ "$psf" == "psfex" ]; then - module="psfex_interp_runner" - elif [ "$psf" == "setools" ]; then - module="setools_runner" - else - module="mccd_interp_runner" - fi - -elif [ "$cat" == "psf_conv" ]; then - - #run_in="$pwd/../P?" - run_in="$pwd" - pattern="validation_psf_conv-*" - module="psfex_interp_runner" - -elif [ "$cat" == "shdu" ]; then - - run_in="$pwd/$out_base/run_sp_exp_Sp_shdu_*" - module="split_exp_runner" - pattern="headers-*" - -else - - echo "Invalid catalogue type $cat" - exit 2 - -fi - - -OUTPUT="$pwd/$out_base/$run_out" -mkdir -p $OUTPUT - - -# Create links - -## target directory -outdir=$OUTPUT/$module/output -mkdir -p $outdir - -## identify source files - -# The following can result in an "Argument list too long" error -#FILES=(`find $run_in -type f -name "$pattern" -print0 | xargs -0 echo`) - -i=0 -for dir in $run_in; do - FILES=(`find -L $dir -type f -name "$pattern" -print0 | xargs -0 echo`) - - #echo "$dir $pattern" - - ## Look over source files - for file in ${FILES[@]}; do - - target=$file - link_name=$outdir/`basename $file` - link_s $target $link_name - ((i=i+1)) - - done - -done - -#echo " $n_files target files, $i links created/skipped" -echo " $i total, "$n_skipped skipped, "$n_created links created" - -# Update log file -update_runs_log_file.py diff --git a/scripts/sh/curl_canfar_local.sh b/scripts/sh/curl_canfar_local.sh deleted file mode 100755 index ada5dc4f0..000000000 --- a/scripts/sh/curl_canfar_local.sh +++ /dev/null @@ -1,303 +0,0 @@ -# Global variables -SSL=~/.ssl/cadcproxy.pem -SESSION=https://ws-uv.canfar.net/skaha/v0/session -IMAGE=images.canfar.net/unions/shapepipe -NAME=shapepipe - -# :TODO: Not working -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source $HOME/shapepipe/scripts/sh/functions.sh - -# Command line arguments - -## Default values -job=-1 -psf="psfex" -ID=-1 -file_IDs=-1 -N_SMP=1 -fix=0 -version="1.1" -cmd_remote="$HOME/shapepipe/scripts/sh/init_run_exclusive_canfar.sh" -batch=30 -batch_max=200 -dry_run=0 -mh_local=0 -sp_local=0 -test_only=0 -debug_out="-1" -scratch="-1" -sm=1 - -pat="- " - -## Help string -usage="Usage: $(basename "$0") -j JOB -[e ID |-f file_IDs] -k KIND [OPTIONS] -\n\nOptions:\n - -h\tthis message\n - -j, --job JOB\tRunning JOB, bit-coded\n - -e, --exclusive ID - \timage ID\n - -f, --file_IDs path - \tfile containing IDs\n - -p, --psf MODEL\n - \tPSF model, one in ['psfex'|'mccd'], default='$psf'\n - -m, --mh_local MH\n - \tmerged header file local (MH=0) or global (MH=1); default is $mh_local\n - -s, --sp_local SP\n - \tsplit local run local (SP=1) or global (SP=0); default is SP=$sp_local\n - --sm SM\n - \tWith (SM=1; default) or without (SM=0) spread model input\n - -N, --N_SMP N_SMOp\n - \tnumber of jobs (SMP mode only), default=$N_SMP\n - -F, --fix FIX\n - \tfix missing data (re-download tile, unzip) for FIX=1; default is $fix\n - -V, --version\n - \tversion of docker image, default='$version'\n - -C, --command_remote\n - \tremote command to run on canfar, default='$cmd_remote'\n - -S, --scratch\n - \tprocessing scratch directory, default is None ($scratch)\n - -b, --batch_max\n - \tmaximum batch size = number of jobs run simultaneously, default=$batch_max\n - --debug_out PATH\n - \tdebug output file PATH, default not used\n - -n, --dry_run LEVEL\n - \tdry run, from LEVEL=2 (no processing) to 0 (full run; default)\n - --test\n - \ttest mode, no processing\n -" - -## Help if no arguments -if [ -z $1 ]; then - echo -ne $usage - exit 1 -fi - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -j|--job) - job="$2" - shift - ;; - -p|--psf) - psf="$2" - shift - ;; - -m|--mh_local) - mh_local="$2" - shift - ;; - -s|--sp_local) - sp_local="$2" - shift - ;; - --sm) - sm="$2" - shift - ;; - -e|--exclusive) - ID="$2" - shift - ;; - -f|--file_IDs) - file_IDs="$2" - shift - ;; - -N|--N_SMP) - N_SMP="$2" - shift - ;; - -F|--fix) - fix="$2" - shift - ;; - -S|--scratch) - scratch="$2" - shift - ;; - -V|--version) - version="$2" - shift - ;; - -B|--batch) - batch="$2" - shift - ;; - -b|--batch_max) - batch_max="$2" - shift - ;; - --debug_out) - debug_out="$2" - shift - ;; - -n|--dry_run) - dry_run="$2" - shift - ;; - --test) - test_only=1 - ;; - esac - shift -done - - -## Check options - -if [ "$test_only" == "1" ]; then - test_arg="--test" -else - test_arg="" -fi - -if [ "$job" == "-1" ]; then - echo "No job indicated, use option -j" - exit 2 -fi - -if [ "$ID" == "-1" ] && [ "$file_IDs" == "-1" ]; then - echo "No image ID(s) indicated, use option -e ID or -f file_IDs" - exit 3 -fi - -if [ "$psf" != "psfex" ] && [ "$psf" != "mccd" ]; then - echo "PSF (option -p) needs to be 'psfex' or 'mccd'" - exit 4 -fi - -if [ "$dry_run" != 0 ] && [ "$dry_run" != 1 ] && [ "$dry_run" != 2 ]; then - echo "Invalid dry_run option, allowed are 0, 1, and 2" - exit 5 -fi - -if [ "$debug_out" != "-1" ]; then - echo "${pat}Starting $(basename "$0") $test_arg" >> $debug_out - echo "${pat}curl ID=$ID" >> $debug_out - echo ${pat}`date` >> $debug_out -fi - -source activate shapepipe -if [ "$debug_out" != "-1" ]; then - echo "${pat}conda prefix = ${CONDA_PREFIX}" >> $debug_out - echo "${pat}script version = ${script_version}" >> $debug_out -fi - -# command line arguments for remote script: -# collect into string - - -RESOURCES="ram=4&cores=$N_SMP" -dir=`pwd` - - -function submit_batch() { - path=$1 - - for ID in `cat $path`; do - IDt=`echo $ID | tr "." "-"` - my_name="SP-${patch}-J${job}-${IDt}" - call_curl $my_name $job $psf $ID $N_SMP $dry_run $dir $mh_local $sp_local $sm $debug_out $fix $scratch $test_arg - done -} - -batch=50 -if [ "$batch" -ge "$batch_max" ]; then - ((batch=batch_max/2)) - echo "Reducing batch size to $batch" -fi -sleep=75 - -((n_thresh=batch_max-batch)) - - -if [ "$dry_run" == 2 ]; then - - # Do not call curl (dry run = 2) - echo "Running command dry run:" - - if [ "$ID" == "-1" ]; then - - - # Submit file (dry run = 2) - for ID in `cat $file_IDs`; do - IDt=`echo $ID | tr "." "-"` - my_name="SP-${patch}-J${job}-${IDt}" - call_curl $my_name $job $psf $ID $N_SMP $dry_run $dir $mh_local $sp_local $sm $debug_out $fix $scratch $test_arg - done - - else - - # Submit image (dry run = 2) - IDt=`echo $ID | tr "." "-"` - my_name="SP-${patch}-J${job}-${IDt}" - call_curl $my_name $job $psf $ID $N_SMP $dry_run $dir $mh_local $sp_local $sm $debug_out $fix $scratch $test_arg - - fi - -else - - # Call curl - rm -rf session_IDs.txt session_image_IDs.txt - - if [ "$ID" == "-1" ]; then - - # Submit file - n_jobs=`cat $file_IDs | wc -l` - if [ "$n_jobs" -gt "$batch_max" ]; then - - # Split into batches - prefix="${file_IDs}_split_" - split -d -l $batch $file_IDs $prefix - n_split=`ls -l $prefix* | wc -l` - echo "Split '$file_IDs' into $n_split batches of size $batch" - - count=1 - n_queued=`stats_jobs_canfar.sh -w all` - for batch in $prefix*; do - echo "Number of queued jobs = $n_queued" - echo "Submitting batch $batch ($count/$n_split)" - echo -ne "\033]0;curl patch=$patch job=$job $count/$n_split\007" - submit_batch $batch - ((count=count+1)) - - n_queued=`stats_jobs_canfar.sh -w all` - - while [ "$n_queued" -gt "$n_thresh" ]; do - echo "Wait for #jobs = $n_queued jobs to go < $n_thresh ..." - sleep $sleep - n_queued=`stats_jobs_canfar.sh -w all` - done - - done - - else - - # Submit entire file (single batch) - echo "Submit '$file_IDs' in single batch" - submit_batch $file_IDs - - fi - - else - - # Submit image - IDt=`echo $ID | tr "." "-"` - my_name="SP-${patch}-J${job}-${IDt}" - call_curl $my_name $job $psf $ID $N_SMP $dry_run $dir $mh_local $sp_local $sm $debug_out $fix $scratch $test_arg - - fi - -fi - -echo "Done $(basename "$0")" - -if [ "$debug_out" != "-1" ]; then - echo "${pat}End $(basename "$0") $test_arg" >> $debug_out -fi diff --git a/scripts/sh/init_run_exclusive_canfar.sh b/scripts/sh/init_run_exclusive_canfar.sh deleted file mode 100755 index 27efadc11..000000000 --- a/scripts/sh/init_run_exclusive_canfar.sh +++ /dev/null @@ -1,642 +0,0 @@ -#!/bin/bash - -# init_run_exclusive_canfar.sh - -# Command line arguments -## Default values -job=-1 -ID=-1 -N_SMP=1 -dry_run=0 -dir=`pwd` -debug_out=-1 -scratch=-1 -fix=0 -test_only=0 -sm=1 - -# mh_local is 0 (1) if merge_header_runner is run on all exposures, -# which is standard so far (run on exposures of given tile only; new) -mh_local=1 - -# sp_local is 0 (1) is split_headers_runner and mask_runner is run -# on all exposures (locally). Not 100% automatic yet. -sp_local=1 -VERBOSE=1 - -pat="-- " - - -## Help string -usage="Usage: $(basename "$0") -j JOB -e ID [OPTIONS] -\n\nOptions:\n - -h\tthis message\n - -j, --job JOB\tRUnning JOB, bit-coded\n - -e, --exclusive ID - \timage ID\n - -p, --psf MODEL\n - \tPSF model, one in ['psfex'|'mccd'], default='$psf'\n - -m, --mh_local MH\n - \tmerge header file local (MH=1) or global (MH=0); default is $mh_local\n - -s, --sp_local SP\n - \tsplit local run local (SP=1) or global (SP=0); default is $sp_local\n - --sm SM\n - \tWith (SM=1; default) or without (SM=0) spread model input\n - -N, --N_SMP N_SMOp\n - \tnumber of jobs (SMP mode only), default from original config files\n - -d, --directory\n - \trun directory, default is pwd ($dir)\n - -S, --scratch\n - \tprocessing scratch directory, default is None ($scratch)\n - -F, --fix FIX\n - \tfix missing data (re-download tile, unzip) for FIX=1; default is $fix\n - -n, --dry_run LEVEL\n - \tdry run (LEVEL=1), no actual processing; default is $dry_run\n - --debug_out PATH\n - \tdebug output file PATH, default not used\n - --test\n - \ttest mode, no processing\n -" - -## Help if no arguments -if [ -z $1 ]; then - echo -ne $usage - exit 1 -fi - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -j|--job) - job="$2" - shift - ;; - -e|--exclusive) - ID="$2" - shift - ;; - -p|--psf) - psf="$2" - shift - ;; - -m|--mh_local) - mh_local="$2" - shift - ;; - -s|--sp_local) - sp_local="$2" - shift - ;; - --sm) - sm="$2" - shift - ;; - -N|--N_SMP) - N_SMP="$2" - shift - ;; - -d|--directory) - dir="$2" - shift - ;; - -S|--scratch) - scratch="$2" - shift - ;; - -n|--dry_run) - dry_run="$2" - shift - ;; - -F|--fix) - fix="$2" - shift - ;; - --debug_out) - debug_out="$2" - shift - ;; - --test) - test_only=1 - ;; - esac - shift -done - - -# functions -function message() { - msg=$1 - my_debug_out=$2 - my_exit=$3 - - echo $msg - if [ "$my_debug_out" != "-1" ]; then - echo ${pat}$msg >> $my_debug_out - fi - - if [ "$my_exit" != "-1" ]; then - echo "${pat}exiting with code $my_exit" >> $my_debug_out - exit $my_exit - fi -} - -# Write an updated copy of a shapepipe config with NUMBER_LIST set to the -# given image ID, expressed in the numbering scheme (leading dash, dots -> -# dashes). Replaces the retired shapepipe_run -e/--exclusive flag (#746). -function set_config_number_list() { - local config_orig=$1 - local config_upd=$2 - local _id=$3 - - local number="-$(echo $_id | tr '.' '-')" - local config_tmp="${config_upd}.tmp" - - if grep -q "^NUMBER_LIST" "$config_orig"; then - perl -pe 's/^NUMBER_LIST\s*=.*/NUMBER_LIST = '$number'/' "$config_orig" > "$config_tmp" - else - perl -pe 's/^\[FILE\][ \t]*$/[FILE]\nNUMBER_LIST = '$number'/' "$config_orig" > "$config_tmp" - fi - if ! grep -q "^NUMBER_LIST = $number$" "$config_tmp"; then - echo "set_config_number_list: failed to set NUMBER_LIST in $config_orig" >&2 - exit 1 - fi - mv "$config_tmp" "$config_upd" -} - - -# Init message -message "test=$test_only" $debug_out -1 -if [ "$test_only" == "1" ]; then - echo "MKDEBUG test" - msg="init_run_exclusive.py script test mode, exiting." - ex=0 -else - msg="init_run_exclusive.py script processing mode, starting." - ex=-1 -fi -message "$msg" $debug_out $ex - - -## Check options -message "checking options" $debug_out -1 - -if [ "$job" == "-1" ]; then - message "No job indicated, use option -j" $debug_out 2 -fi - -if [ "$ID" == "-1" ]; then - message "No image ID indicated, use option -e" $debug_out 3 -fi - -if [ "$psf" != "psfex" ] && [ "$psf" != "mccd" ]; then - message "PSF (option -p) needs to be 'psfex' or 'mccd'" $debug_out 4 -fi - -if [ "$mh_local" != "0" ] && [ "$mh_local" != "1" ]; then - message "mh_local (option -m) needs to be 0 or 1" $debug_out 5 -fi - -if [ "$sp_local" != "0" ] && [ "$sp_local" != "1" ]; then - message "sp_local (option -m) needs to be 0 or 1" $debug_out 6 -fi - -if [ "$dry_run" != "0" ] && [ "$dry_run" != 1 ]; then - message "dry_run must be 0 or 1, not $dry_run" $debug_out 8 -fi - - -# Start script - -source $HOME/shapepipe/scripts/sh/functions.sh - -msg="Starting $(basename "$0") `date` ID=$ID" -message "$msg" $debug_out -1 -#message "`date`" $debug_out -1 -#message "ID=$ID" $debug_out -1 - -# Set kind -kind=$(get_kind_from_job $job) - -if [ "$kind" == "none" ]; then - message "Error: invalid job $job" $debug_out 5 -fi - -message "kind=$kind" $debug_out -1 - - -if [ "$dry_run" == "1" ]; then - message "running in dry run mode" $debug_out -1 -else - message "not running in dry run mode" $debug_out -1 -fi - -CONDA_PREFIX=$HOME/.conda/envs/shapepipe -PATH=$PATH:$CONDA_PREFIX/bin - -cd $dir - -if [ ! -d ${kind}_runs ]; then - command "mkdir ${kind}_runs" $dry_run -fi - -if [ "$fix" == "1" ]; then - message "Fixing missing data" $debug_out -1 - - message "Download tile images" $debug_out -1 - cd data_tiles - file_name=CFIS.$ID.r.fits - if [ ! -e $file_name ]; then - message "Downloading $file_name" $debug_out -1 - vcp vos:cfis/tiles_DR5/$file_name . - else - message "File $file_name exists, skipping" $debug_out -1 - fi - file_name=CFIS.$ID.r.weight.fits.fz - if [ ! -e $file_name ]; then - message "Downloading $file_name" $debug_out -1 - vcp vos:cfis/tiles_DR5/$file_name . - else - message "File $file_name exists, skipping" $debug_out -1 - fi - cd .. - - message "link to tiles..." $debug_out -1 - cd output/run_sp_GitFeGie_202*/get_images_runner_run_1/output - IDt=`echo $ID | tr "." "-"` - path_tiles=/arc/home/kilbinger/cosmostat/v2/pre_v2/$psf/$patch/data_tiles - link_name=CFIS_image-$IDt.fits - if [ ! -e $link_name ]; then - message "Creating link $link_name" $debug_out -1 - # force to overwrite broken link - ln -sf $path_tiles/CFIS.$ID.r.fits $link_name - else - message "Link $link_name exists, skipping" $debug_out -1 - fi - - link_name=CFIS_weight-$IDt.fitsfz - if [ ! -e $link_name ]; then - message "Creating link $link_name" $debug_out -1 - ln -sf $path_tiles/CFIS.$ID.r.weight.fits.fz $link_name - else - message "Link $link_name exists, skipping" $debug_out -1 - fi - - command "cd ../../../.." $dry_run - - message "Unzip weight ($dry_run)" $debug_out -1 - command "cd tile_runs/$ID" $dry_run - export SP_RUN=`pwd` - command "set_config_number_list cfis/config_tile_Uz.ini config_tile_Uz_upd.ini $ID" $dry_run - command "shapepipe_run -c config_tile_Uz_upd.ini" $dry_run - - cd $dir -else - message "Not fixing missing data" $debug_out -1 -fi - - -cd ${kind}_runs - -if [ ! -d "$ID" ]; then - command "mkdir $ID" $dry_run -fi - -cd $ID -pwd - -# Point cfis to local link, to be independent of platform -ln -sf ~/shapepipe/example/cfis - - -if [ ! -d "output" ]; then - command "mkdir output" $dry_run -fi - -cd output - - -# Update links to global run directories (GiFeGie) -# New: 27/11/2024: Remove link to Uz, conflict with fix -for my_dir in $dir/output/run_sp_[G]*; do - command "ln -sf $my_dir" $dry_run -done - -# The following could be done also with fix=1 -if [ "$fix" == "0" ] && [ "$kind" == "tile" ]; then - if [ ! -e "run_sp_Uz*" ]; then - - message "previous uncompress run not found..." $debug_out -1 - - # Remove broken links - for my_dir in run_sp_Uz*; do - if [ -L $my_dir ]; then - command "rm $my_dir" $dry_run - message "remove broken link $my_dir" $debug_out -1 - fi - done - - # Create working link - for my_dir in $dir/output/run_sp_Uz*; do - command "ln -sf $my_dir" $dry_run - message "create valid link $my_dir" $debug_out -1 - done - cd .. - command "update_runs_log_file.py" $dry_run - cd output - fi -fi - - -# Combined flags - -## Tiles -command "ln -sf $dir/output/run_sp_Ma_tile" $dry_run - -if [ "$sp_local" == "0" ]; then - # Exposures - command "ln -sf $dir/output/run_sp_Ma_exp" $dry_run -#else - #command "rm -f $dir/output/run_sp_Ma_exp" $dry_run -fi - -# Check for existing exp SpMh dir -dir_SpMh="run_sp_exp_SpMh" -if [[ -d "$dir/output/$dir_SpMh" ]]; then - # create link - command "ln -sf $dir/output/$dir_SpMh" $dry_run -else - message "No global SpMh directory found" $debug_out -1 - if [ -L "$dir_SpMh" ] && [ ! -e "$dir_SpMh" ]; then - message "Remove broken link $dir_SpMh" $debug_out -1 - rm $dir_SpMh - fi -fi - -(( do_job = $job & 2 )) -if [ $do_job != 0 ] && [ "$sp_local" == "1" ]; then - message "run local sp" $debug_out -1 - command "rm -rf run_sp_Gie*" $dry_run - command "rm -rf run_sp_exp_Sp*" $dry_run - - # Create new get_image dir - new_dir="run_sp_Gie/get_images_runner/output" - command "rm -rf $new_dir" $dry_run - command "mkdir -p $new_dir" $dry_run - - # Link to image, weight, and flag file of current exposure - - # Remove HDU extension - command "cd $new_dir" $dry_run - exp_ID=$(echo "$ID" | sed 's/-[0-9]\{1,2\}//') - for file in $dir/output//run_sp_GitFeGie_20*/get_images_runner_run_2/output/*${exp_ID}.* ; do - echo $file - command "ln -s $file" $dry_run - done - command "cd ../../.." $dry_run - - # Run Sp - command "cd .." $dry_run - if [ "$scratch" != "-1" ]; then - command "cd ../.." $dry_run - command "mkdir -p $scratch/exp_runs" $dry_run - command "cp -R exp_runs/$ID $scratch/exp_runs" $dry_run - command "cd $scratch/exp_runs/$ID" $dry_run - fi - command "update_runs_log_file.py" $dry_run - export SP_RUN=`pwd` - command "set_config_number_list cfis/config_exp_Sp.ini config_exp_Sp_upd.ini $exp_ID" $dry_run - command "shapepipe_run -c config_exp_Sp_upd.ini" $dry_run - - # Only keep CCD of this ID - command "mkdir -p output/run_sp_exp_Sp_shdu/split_exp_runner/output" $dry_run - command "mv output/run_sp_exp_Sp/split_exp_runner/output/*$ID.* output/run_sp_exp_Sp_shdu/split_exp_runner/output" $dry_run - command "mv output/run_sp_exp_Sp/split_exp_runner/output/headers* output/run_sp_exp_Sp_shdu/split_exp_runner/output" $dry_run - command "rm -rf output/run_sp_exp_Sp" $dry_run - if [ "$scratch" != "-1" ]; then - command "mv output/run_sp_exp_Sp_shdu $dir/exp_runs/$ID/output" $dry_run - command "cd .." $dry_run - command "rm -rf $ID" $dry_run - command "cd $dir" $dry_run - fi - command "update_runs_log_file.py" $dry_run - cd output - - if [ "$job" == "2" ]; then - msg="Finishing $(basename "$0") after job=2 `date` ID=$ID" - message "$msg" $debug_out 0 - fi - -fi - -if [ "$kind" == "tile" ] && [ "$sp_local" == "1" ]; then - echo "New (for P9): skipping link_to_exp_for_tile.py" - cd ../../.. - #command "link_to_exp_for_tile.py -t $ID -i tile_runs -I exp_runs -s $sp_local" $dry_run - cd tile_runs/$ID - #command "combine_runs.bash -p psfex -c shdu" $dry_run - cd output -fi - - -if [ "$mh_local" == "0" ]; then - if [ ! -f log_exp_headers.sqlite ]; then - # Global Mh and file does not exist -> symlink to - # gllobal mh file - message "creating global link to exp headers (mh_local=0)" $debug_out -1 - command "ln -s $dir/output/log_exp_headers.sqlite" $dry_run - else - message "global link to exp headers (mh_local=0) exists" $debug_out -1 - fi -else - # Local Mh - message "not creating global link to exp headers (mh_local=1)" $debug_out -1 - if [ "$ID" == "-1" ]; then - message "ID needs to be given (option -e) for mh_local" $debug_out 6 - fi - - # Check and remove symbolic (global) mh file link - if [ -L log_exp_headers.sqlite ]; then - # Local Mh and symlink -> remove previous link to - # (potentially incomplete) global mh file - message "Removing previous mh sym link" $debug_out -1 - command "rm log_exp_headers.sqlite" $dry_run - else - message "no mh link found" $debug_out -1 - fi - - # Check size of existing header file - if [ -e log_exp_headers.sqlite ]; then - size=$(stat -c %s log_exp_headers.sqlite) - if (( size > 15000 )); then - message "Found valid local mh file, continuing" $debug_out -1 - else - message "Existing local mh file looks invalid, deleting" $debug_out -1 - rm -f log_exp_headers.sqlite - fi - fi - - if [ ! -e log_exp_headers.sqlite ]; then - message "Creating local mh file" $debug_out -1 - - cd .. - command "update_runs_log_file.py" $dry_run - export SP_RUN=`pwd` - command "shapepipe_run -c cfis/config_exp_Mh.ini" $dry_run - cd output - else - message "Found local mh file, continuing" $debug_out -1 - fi -fi - - -(( do_job = $job & 8 )) -if [ $do_job != 0 ] && [ "$sp_local" == "1" ]; then - # Remove previous local Ma runs - message "cdsclient = $CDSCLIENT" $debug_out -1 - command "rm -rf run_sp_exp_Ma*" $dry_run -fi - -(( do_job = $job & 16 )) -if [[ $do_job != 0 ]]; then - # Remove previous Sx runs - command "rm -rf run_sp_tile_Sx_*" $dry_run -fi - -# Update links to exposure run directories, which were created in job 32 -(( do_job = $job & 64 )) -if [[ $do_job != 0 ]]; then - if [ "$kind" == "tile" ]; then - - # Remove previous runs of this job - rm -rf run_sp_tile_PsViSmVi* - fi -fi - -(( do_job = $job & 128 )) -if [[ $do_job != 0 ]]; then - - echo - - cat_ngmix="run_sp_tile_ngmix_Ng1u/ngmix_runner/output/ngmix-*.fits" - dir_ngmix_prev="run_sp_tile_ngmix_Ng1u_prev/ngmix_runner/output" - cat_ngmix_prev="$dir_ngmix_prev/ngmix-*.fits" - - # Remove if empty - if [ ! -s $cat_ngmix ]; then - echo "Removing empty file $cat_ngmix" - rm $cat_ngmix - fi - if [ ! -s $cat_ngmix_prev ]; then - echo "Removing empty file $cat_ngmix_prev" - rm $cat_ngmix_prev - fi - - # Check whether ngmix output exists - if [ -e $cat_ngmix ]; then - message "ngmix output catalogue exists" $debug_out -1 - - # Check whether previous ngmix directory and output cat exist - exists="1" - if [ ! -d $dir_ngmix_prev ]; then - exists="0" - elif [ ! -e "$cat_ngmix_prev" ]; then - exists="1" - fi - if [ "$exists" == "0" ]; then - message "Moving to previous batch-save dir (does not exist yet)" $debug_out -1 - command "mkdir -p $dir_ngmix_prev" $dry_run - command "mv $cat_ngmix $dir_ngmix_prev" $dry_run - else - # Compare file sizes - size_cat_ngmix=$(stat -c%s $cat_ngmix) - size_cat_ngmix_prev=$(stat -c%s $cat_ngmix_prev) - if [ "$size_cat_ngmix" -gt "$size_cat_ngmix_prev" ]; then - message "Moving to batch-save dir, overwriting smaller batch-save cat" $debug_out -1 - command "mv $cat_ngmix $dir_ngmix_prev" $dry_run - else - message "Previous batch-save dir not smaller, removing ngmix output" $debug_out -1 - command "rm $cat_ngmix" $dry_run - fi - fi - else - # Whether or not previous ngmix exists, job_sp_canfar will handle it - message "No ngmix output exists, continuing..." $debug_out -1 - fi - - echo -fi - -(( do_job = $job & 256 )) -if [[ $do_job != 0 ]]; then - - # Remove previous runs of this job - rm -rf run_sp_Ms_20??-* - -fi - -(( do_job = $job & 512 )) -if [[ $do_job != 0 ]]; then - - # Remove previous runs of this job - rm -rf run_sp_Mc_20??-* - -fi - - -cd .. - -# Update log file -command update_runs_log_file.py $dry_run - -echo -n "pwd: " -pwd - -echo -n "environment: " -echo $CONDA_PREFIX - -# To avoid (new?) qt error with setools (-j 32) -export DISPLAY=:1.0 - - -if [ "$scratch" != "-1" ]; then - # Copy inputs to scratch - command "mkdir -p $scratch/${kind}_runs" $dry_run - cd ../.. - command "pwd" $dry_run - command "cp -R ${kind}_runs/$ID $scratch/${kind}_runs" $dry_run - command "cd $scratch/${kind}_runs/$ID" $dry_run -fi - -command "job_sp_canfar.bash -p psfex -j $job -e $ID --n_smp $N_SMP --nsh_jobs $N_SMP --debug_out $debug_out --sm $sm " $dry_run - -if [ "$scratch" != "-1" ]; then - cd ../.. - if [ "$job" == "16" ]; then - command "mv ${kind}_runs/$ID/output/run_sp_Sx_* $dir/${kind}_runs/$ID/output" $dry_run - elif [ "$job" == "32" ]; then - command "mv ${kind}_runs/$ID/output/run_sp_exp_SxSe* $dir/${kind}_runs/$ID/output" $dry_run - elif [ "$job" == "64" ]; then - command "mv ${kind}_runs/$ID/output/run_sp_tile_PsViSm** $dir/${kind}_runs/$ID/output" $dry_run - elif [ "$job" == "128" ]; then - command "mv ${kind}_runs/$ID/output/run_sp_tile_ngmix* $dir/${kind}_runs/$ID/output" $dry_run - else - echo "scratch mode with job=$job not implemented yet, exiting..." - exit 8 - fi - - command "rm -rf ${kind}_runs/$ID" $dry_run - command "cd $dir/${kind}_runs/$ID" $dry_run - command "update_runs_log_file.py" $dry_run - command "cd $dir" $dry_run -fi - -cd $dir - -#msg="End $(basename "$0")" - -msg="Finished $(basename "$0") `date` ID=$ID" -message "$msg" $debug_out -1 diff --git a/scripts/sh/job_sp_canfar.bash b/scripts/sh/job_sp_canfar.bash deleted file mode 100755 index c99435823..000000000 --- a/scripts/sh/job_sp_canfar.bash +++ /dev/null @@ -1,610 +0,0 @@ -#!/bin/bash - -# Name: job_sp_canfar.bash -# Description: General script to process one or more tiles -# with all contributing exposures. -# This works as job submission script for -# the canfar batch system. -# called in interactive mode on a virtual -# machine. -# Author: Martin Kilbinger - - -# Command line arguments -## Default values -job=255 -config_dir=$HOME/shapepipe/example/cfis -psf='mccd' -retrieve='vos' -star_cat_for_mask='onthefly' -exclusive='' -results='cosmostat/kilbinger/results_v2' -n_smp=-1 -nsh_jobs=8 -debug_out=-1 -sm=1 - -pat="--- " - -## Help string -usage="Usage: $(basename "$0") [OPTIONS] [TILE_ID] -\n\nOptions:\n - -h\tthis message\n - -j, --job JOB\tRunning JOB, bit-coded\n - \t 1: retrieve images (online if method=vos)\n - \t 2: prepare images (offline)\n - \t 4: mask tiles (online if star_cat_for_mask=onthefly)\n - \t 8: mask exposures (online if star_cat_for_mask=onthefly)\n - \t 16: detection of galaxies on tiles (offline)\n - \t 32: processing of stars on exposures (offline)\n - \t 64: galaxy selection on tiles (offline)\n - \t 128: shapes and morphology (offline)\n - \t 256: paste catalogues (offline)\n - -c, --config_dir DIR\n - \t config file directory, default='$config_dir'\n - -p, --psf MODEL\n - \tPSF model, one in ['psfex'|'mccd'], default='$psf'\n - -r, --retrieve METHOD\n - \tmethod to retrieve images, allowed are 'vos', 'symlink', default='$retrieve'\n - -s, --star_cat_for_mask\n - \tcatalogue for masking bright stars, allowed are 'onthefly', 'save',\n - \tdefault is '${star_cat_for_mask}'\n - --sm SM\n - \tWith (SM=1; default) or without (SM=0) spread model input\n - -e, --exclusive ID\n - \texclusive input filer number string ID (default: None)\n - -o, --output_dir\n - \toutput (upload) directory on vos:cfis, default='$results'\n - -n, --n_smp N_SMP\n - \tnumber of jobs (SMP mode only), default from original config files\n - --nsh_jobs NJOB\n - \tnumber of objects per parallel shape module call, \n - \tdefault: optimal number is computed\n - --debug_out PATH\n - \tdebug output file PATH, default not used\n - TILE_ID_i\n - \ttile ID(s), e.g. 283.247 214.242, only with '-j 1'\n -" - -## Help if no arguments -if [ -z $1 ]; then - echo -ne $usage - exit 1 -fi - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -j|--job) - job="$2" - shift - ;; - -c|--config_dir) - config_dir="$2" - shift - ;; - -p|--psf) - psf="$2" - shift - ;; - -r|--retrieve) - retrieve="$2" - shift - ;; - -s|--star_cat_for_mask) - star_cat_for_mask="$2" - shift - ;; - --sm) - sm="$2" - shift - ;; - -e|--exclusive) - exclusive="$2" - shift - ;; - -o|--output_dir) - results="$2" - shift - ;; - -n|--n_smp) - n_smp="$2" - shift - ;; - --nsh_jobs) - nsh_jobs="$2" - shift - ;; - --debug_out) - debug_out="$2" - shift - ;; - esac - shift -done - -## Check options -if [ "$psf" != "psfex" ] && [ "$psf" != "mccd" ]; then - echo "PSF (option -p) needs to be 'psfex' or 'mccd'" - exit 2 -fi - -if [ "$star_cat_for_mask" != "onthefly" ] && [ "$star_cat_for_mask" != "save" ]; then - echo "Star cat for mask (option -s) needs to be 'onthefly' or 'save'" - exit 4 -fi - -if [ "$retrieve" != "vos" ] && [ "$retrieve" != "symlink" ]; then - echo "method to retrieve images (option -r) needs to be 'vos' or 'symlink'" - exit 5 -fi - -if [ "$debug_out" != "-1" ]; then - echo $pat`date` >> $debug_out - echo "${pat}Starting $(basename "$0")" >> $debug_out -fi - -CONDA_PREFIX=/arc/home/kilbinger/.conda/envs/shapepipe -PATH=$PATH:$CONDA_PREFIX/bin - -# For tar archives. TODO: Should be unique to each job -export ID="test" - -## Paths - -## Path variables used in shapepipe config files - -# Run path and location of input image directories -export SP_RUN=`pwd` - -# Config file path -export SP_CONFIG=$SP_RUN/cfis -export SP_CONFIG_MOD=$SP_RUN/cfis_mod - -## Other variables - -# Output -OUTPUT=$SP_RUN/output - -# For tar archives -output_rel=`realpath --relative-to=. $OUTPUT` - -# Stop on error, default=1 -STOP=1 - -# Verbose mode (1: verbose, 0: quiet) -VERBOSE=1 - -# VCP options -export CERTFILE=$HOME/.ssl/cadcproxy.pem -export VCP="vcp --certfile=$CERTFILE" - - -## Functions - -# Print string, executes command, and prints return value. -function command () { - cmd=$1 - str=$2 - - RED='\033[0;31m' - GREEN='\033[0;32m' - NC='\033[0m' # No Color - # Color escape characters show up in log files - #RED='' - #GREEN='' - #NC='' - - - if [ "$debug_out" != "-1" ]; then - echo "${pat}pwd = `pwd`" >> $debug_out - echo "${pat}SP_RUN = $SP_RUN" >> $debug_out - echo "${pat}SP_CONFIG = $SP_CONFIG" >> $debug_out - fi - - if [ $# == 2 ]; then - if [ $VERBOSE == 1 ]; then - echo "$str: running '$cmd'" - fi - if [ "$debug_out" != "-1" ]; then - echo "${pat}Running $cmd" >> $debug_out - fi - - $cmd - - else - if [ $VERBOSE == 1 ]; then - echo "$str: running '$cmd $4 \"$5 $6\"'" - fi - if [ "$debug_out" != "-1" ]; then - echo "${pat}Running $cmd $4 \"$5 $6\"" >> $debug_out - fi - - $cmd $4 "$5 $6" - - fi - - res=$? - - if [ "$debug_out" != "-1" ]; then - echo "${pat}exit code = $res" >> $debug_out - fi - - if [ $VERBOSE == 1 ]; then - if [ $res == 0 ]; then - echo -e "${GREEN}success, return value = $res${NC}" - else - echo -e "${RED}error, return value = $res${NC}" - if [ $STOP == 1 ]; then - echo "${RED}exiting 'canfar_sp.bash', error in command '$cmd'${NC}" - exit $res - else - echo "${RED}continuing 'canfar_sp.bash', error in command '$cmd'${NC}" - fi - fi - fi -} - -# Run shapepipe command. If error occurs, upload sp log files before stopping script. -function command_sp() { - local cmd=$1 - local str=$2 - - command "$1" "$2" -} - -# Set up config file and call shapepipe_run -function command_cfg_shapepipe() { - local config_name=$1 - local str=$2 - local _n_smp=$3 - local _exclusive=$4 - - config_upd=$(set_config_n_smp $config_name $_n_smp) - - # Run a single image ID via NUMBER_LIST in an updated config copy; - # replaces the retired shapepipe_run -e/--exclusive flag (#746) - if [ "$_exclusive" != "" ]; then - set_config_number_list "$config_upd" "$SP_CONFIG_MOD/$config_name" "$_exclusive" - config_upd="$SP_CONFIG_MOD/$config_name" - fi - - local cmd="shapepipe_run -c $config_upd" - command_sp "$cmd" "$str" -} - -# Tar and upload files to vos -function upload() { - base=$1 - shift - ID=$1 - shift - verbose=$1 - shift - upl=("$@") - - echo "Counting upload files" - n_upl=(`ls -l ${upl[@]} | wc`) - if [ $n_upl == 0 ]; then - if [ $STOP == 1 ]; then - echo "Exiting script, no file found for '$base' tar ball" - exit 3 - fi - fi - tar czf ${base}_${ID}.tgz ${upl[@]} - command "$VCP ${base}_${ID}.tgz vos:cfis/$results" "Upload tar ball" -} - -# Upload log files -function upload_logs() { - id=$1 - verbose=$2 - - upl="$output_rel/*/*/logs $output_rel/*/logs" - upload "logs" "$id" "$verbose" "${upl[@]}" -} - -function set_config_n_smp() { - local config_name=$1 - local _n_smp=$2 - - local config_orig="$SP_CONFIG/$config_name" - - if [[ $_n_smp != -1 ]]; then - # Update SMP batch size - local config_upd="$SP_CONFIG_MOD/$config_name" - update_config $config_orig $config_upd "SMP_BATCH_SIZE" $_n_smp - else - # Keep original config file - local config_upd=$config_orig - fi - - # Set "return" value (stdout) - echo "$config_upd" -} - -# Update config file -function update_config() { - local config_orig=$1 - local config_upd=$2 - local key=$3 - local val_upd=$4 - - cat $config_orig \ - | perl -ane 's/'$key'\s+=.+/'$key' = '$val_upd'/; print' > $config_upd -} - -# Write an updated copy of a shapepipe config with NUMBER_LIST set to the -# given image ID, expressed in the numbering scheme (leading dash, dots -> -# dashes). Replaces the retired shapepipe_run -e/--exclusive flag (#746). -function set_config_number_list() { - local config_orig=$1 - local config_upd=$2 - local _id=$3 - - local number="-$(echo $_id | tr '.' '-')" - local config_tmp="${config_upd}.tmp" - - if grep -q "^NUMBER_LIST" "$config_orig"; then - perl -pe 's/^NUMBER_LIST\s*=.*/NUMBER_LIST = '$number'/' "$config_orig" > "$config_tmp" - else - perl -pe 's/^\[FILE\][ \t]*$/[FILE]\nNUMBER_LIST = '$number'/' "$config_orig" > "$config_tmp" - fi - if ! grep -q "^NUMBER_LIST = $number$" "$config_tmp"; then - echo "set_config_number_list: failed to set NUMBER_LIST in $config_orig" >&2 - exit 1 - fi - mv "$config_tmp" "$config_upd" -} - -### Start ### - -echo "Start processing" - -# Create input and output directories -mkdir -p $SP_RUN -cd $SP_RUN -mkdir -p $OUTPUT -mkdir -p $SP_CONFIG_MOD - -# Processing - - -### Retrieve config files -if [[ $config_dir == *"vos:"* ]]; then - command_sp "$VCP $config_dir ." "Retrieve shapepipe config files" -else - if [[ ! -L cfis ]]; then - command_sp "ln -s $config_dir cfis" "Retrieve shapepipe config files" - fi -fi - - -## Retrieve config files and images (online if retrieve=vos) -## Retrieve and save star catalogues for masking (if star_cat_for_mask=save) -(( do_job = $job & 1 )) -if [[ $do_job != 0 ]]; then - - ### Retrieve files - command_cfg_shapepipe \ - "config_GitFeGie_$retrieve.ini" \ - "Retrieve images" \ - -1 \ - $exclusive - - ### Retrieve and save star catalogues for masking - if [ "$star_cat_for_mask" == "save" ]; then - #### For tiles - mkdir $SP_RUN/star_cat_tiles - command_sp \ - "create_star_cat $SP_RUN/output/run_sp_GitFeGie_*/get_images_runner_run_1/output $SP_RUN/star_cat_tiles" \ - "Save star cats for masking (tile)" - - #### For single-exposures - mkdir $SP_RUN/star_cat_exp - command_sp \ - "create_star_cat $SP_RUN/output/run_sp_GitFeGie_*/get_images_runner_run_2/output $SP_RUN/star_cat_exp exp" \ - "Save star cats for masking (exp)" - fi - -fi - -## Prepare images (offline) -(( do_job = $job & 2 )) -if [[ $do_job != 0 ]]; then - - ### Uncompress tile weights - command_cfg_shapepipe "config_tile_Uz.ini" "Run shapepipe (uncompress tile weights)" $n_smp $exclusive - - ### Split images into single-HDU files, merge headers for WCS info - command_cfg_shapepipe \ - "config_exp_SpMh.ini" \ - "Run shapepipe (split images, merge headers)" \ - $n_smp \ - $exclusive - -fi - -## Mask tiles: add star, halo, and Messier object masks (online if "star_cat_for_mask" is "onthefly") -(( do_job = $job & 4 )) -if [[ $do_job != 0 ]]; then - - ### Mask tiles - command_cfg_shapepipe \ - "config_tile_Ma_$star_cat_for_mask.ini" \ - "Run shapepipe (mask tiles)" \ - $n_smp \ - $exclusive - -fi - -## Mask exposures: add star, halo, and Messier object masks (online if "star_cat_for_mask" is "onthefly") -(( do_job = $job & 8 )) -if [[ $do_job != 0 ]]; then - - ### Mask exposures - command_cfg_shapepipe \ - "config_exp_Ma_$star_cat_for_mask.ini" \ - "Run shapepipe (mask exposures)" \ - $n_smp \ - $exclusive - -fi - - -## Remaining exposure processing (offline) -(( do_job = $job & 16 )) -if [[ $do_job != 0 ]]; then - - ### Object detection on tiles - command_cfg_shapepipe \ - "config_tile_Sx.ini" \ - "Run shapepipe (tile detection)" \ - $n_smp \ - $exclusive - -fi - -## Exposure processing (offline) -(( do_job = $job & 32 )) -if [[ $do_job != 0 ]]; then - - ### Star detection, selection, PSF model. setools can exit with an error for CCD with insufficient stars, - ### the script should continue - STOP=0 - command_cfg_shapepipe \ - "config_exp_${psf}.ini" \ - "Run shapepipe (exp $psf)" \ - $n_smp \ - $exclusive - STOP=1 - -fi - -## Process tiles up to shape measurement -(( do_job = $job & 64 )) -if [[ $do_job != 0 ]]; then - - ### PSF model letter: 'P' (psfex) or 'M' (mccd) - letter=${psf:0:1} - Letter=${letter^} - command_cfg_shapepipe \ - "config_tile_${Letter}iViSmVi_canfar.ini" \ - "Run shapepipe (tile PsfInterp=$Letter}: up to ngmix+galsim)" \ - $n_smp \ - $exclusive - -fi - -## Shape measurement (offline) -(( do_job = $job & 128 )) -if [[ $do_job != 0 ]]; then - - ### Prepare config files - n_min=0 - n_obj=`get_number_objects` - if [ "$n_obj" == "-1" ]; then - echo "No tile SExtractor run found, exiting after et_number_objects call" - exit 10 - fi - nsh_step=`echo "$(($n_obj/$nsh_jobs))"` - - n_max=$((nsh_step - 1)) - for k in $(seq 1 $nsh_jobs); do - cat $SP_CONFIG/config_tile_Ng_template_batch.ini | \ - perl -ane \ - 's/(ID_OBJ_MIN =) X/$1 '$n_min'/; s/(ID_OBJ_MAX =) X/$1 '$n_max'/; s/NgXu/Ng'$k'u/; s/X_interp/'$psf'_interp/g; print' \ - > $SP_CONFIG_MOD/config_tile_Ng${k}u.ini - n_min=$((n_min + nsh_step)) - if [ "$k" == $((nsh_jobs - 1)) ]; then - n_max=-1 - else - n_max=$((n_min + nsh_step - 1)) - fi - done - - ### Shapes, run $nsh_jobs parallel processes - VERBOSE=0 - for k in $(seq 1 $nsh_jobs); do - - # if output dir for subrun exists but no output: re-run - ngmix_run=$OUTPUT/"run_sp_tile_ngmix_Ng${k}u/ngmix_runner" - if [ -e "$ngmix_run" ]; then - ngmix_out="$ngmix_run/output" - n_out=`ls -rlt $ngmix_out | wc -l` - if [ "$n_out" -lt 2 ]; then - command \ - "rm -rf $OUTPUT/run_sp_tile_ngmix_Ng${k}u" \ - "Re-running existing empty ngmix subrun $k" - command_sp \ - "shapepipe_run -c $SP_CONFIG_MOD/config_tile_Ng${k}u.ini" \ - "Run shapepipe (tile: ngmix $k)" & - else - echo "Skipping existing non-empty ngmix subrun $k" - fi - else - command_sp \ - "shapepipe_run -c $SP_CONFIG_MOD/config_tile_Ng${k}u.ini" \ - "Run shapepipe (tile: ngmix $k)" & - fi - done - wait - VERBOSE=1 - -fi - -## Create final catalogues (offline) -(( do_job = $job & 256 )) -if [[ $do_job != 0 ]]; then - - cat $SP_CONFIG/config_merge_sep_cats_template.ini | \ - perl -ane \ - 's/(N_SPLIT_MAX =) X/$1 '$nsh_jobs'/; print' \ - > $SP_CONFIG_MOD/config_merge_sep_cats.ini - - ### Merge separated shapes catalogues - command_sp \ - "shapepipe_run -c $SP_CONFIG_MOD/config_merge_sep_cats.ini" \ - "Run shapepipe (tile: merge sep cats)" \ - "$VERBOSE" \ - "$ID" -fi - -(( do_job = $job & 512 )) -if [[ $do_job != 0 ]]; then - - # spread_model suffix for config file with or without SM input - if [ "$sm" == "0" ]; then - suff_sm="_nosm" - else - suff_sm="" - fi - - ### Merge all relevant information into final catalogue - command_cfg_shapepipe \ - "config_make_cat_$psf${suff_sm}.ini" \ - "Run shapepipe (tile: create final cat $psf)" \ - $n_smp \ - $exclusive - -fi - -# MKDEBUG: Putting Mh at the end for now, could be integrated before 16. -(( do_job = $job & 1024 )) -if [[ $do_job != 0 ]]; then - - command_cfg_shapepipe \ - "config_exp_Mh.ini" \ - "Run shapepipe (merge exp headers)" \ - $n_smp \ - $exclusive - -fi - -if [ "$debug_out" != "-1" ]; then - echo "${pat}End $(basename "$0") ID=$exclusive success" >> $debug_out -fi diff --git a/scripts/sh/remove_duplicates_tiles.sh b/scripts/sh/remove_duplicates_tiles.sh deleted file mode 100755 index 6f789fe6e..000000000 --- a/scripts/sh/remove_duplicates_tiles.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env bash - -# Script to remove duplicate tile runs, keeping only the newest -# Run from directory containing P?/tile_runs subdirectories - -count=0 -total_removed=0 - -echo "Starting duplicate tile run removal..." -echo "Looking for patches in P?/tile_runs structure..." -echo "" - -# Process each patch directory -for patch_dir in P*/tile_runs; do - if [ ! -d "$patch_dir" ]; then - continue - fi - - patch_name=$(dirname "$patch_dir") - echo "Processing patch: $patch_name" - - # Process each tile directory within the patch - for dir in "$patch_dir"/*; do - if [ ! -d "$dir" ]; then - continue - fi - - # Check each of the three run types - for run_type in run_sp_tile_PsViSmVi run_sp_tile_Mc run_sp_tile_Ms; do - if compgen -G "$dir/output/${run_type}*" > /dev/null; then - n=$(ls -dt "$dir/output/${run_type}"* 2>/dev/null | wc -l) - if [ "$n" -gt 1 ]; then - ((n_remove=n-1)) - echo " Found $n copies of $run_type in $(basename "$dir"), removing $n_remove oldest" - - # Remove all but the newest - ls -dt "$dir/output/${run_type}"* | tail -n "$n_remove" | while read old_run; do - echo " Removing: $(basename "$old_run")" - rm -rf "$old_run" - ((total_removed++)) - done - - ((count++)) - fi - fi - done - done - echo "" -done - -echo "========================================" -echo "Summary:" -echo " Processed tile directories: $count" -echo " Total run directories removed: $total_removed" -echo "========================================" diff --git a/scripts/sh/run_scratch_local.sh b/scripts/sh/run_scratch_local.sh deleted file mode 100755 index 1223e6683..000000000 --- a/scripts/sh/run_scratch_local.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash - -# Command line arguments -## Default values -job=-1 -ID=-1 -N_SMP=1 -dry_run=0 -dir=`pwd` -debug_out=-1 -scratch=/n17data/`whoami`/scratch -exec_path=$HOME/shapepipe/scripts/sh -slurm=1 - -# mh_local is 0 (1) if merge_header_runner is run on all exposures, -# which is standard so far (run on exposures of given tile only; new) -mh_local=0 - -# sp_local is 0 (1) is split_headers_runner and mask_runner is run -# on all exposures (locally). Not 100% automatic yet. -sp_local=1 -VERBOSE=1 - -pat="-- " - -# Help string -usage="Usage: $(basename "$0") -j JOB -e ID -k KIND [OPTIONS] -\n\nOptions:\n - -h\tthis message\n - -j, --job JOB\tRUnning JOB, bit-coded\n - -e, --exclusive ID - \timage ID\n - -p, --psf MODEL\n - \tPSF model, one in ['psfex'|'mccd'], default='$psf'\n - -m, --mh_local MH\n - \tmerged header file local (MH=0) or global (MH=1); default is $mh_local\n - -N, --N_SMP N_SMOp\n - \tnumber of jobs (SMP mode only), default from original config files\n - -d, --directory\n - \trun directory, default is pwd ($dir)\n - -S, --scratch\n - \tprocessing scratch directory, default is $scratch\n - -n, --dry_run LEVEL\n - \tdry run, no actuall processing\n - --debug_out PATH\n - \tdebug output file PATH, default not used\n -" - -## Help if no arguments -if [ -z $1 ]; then - echo -ne $usage - exit 1 -fi - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -j|--job) - job="$2" - shift - ;; - -e|--exclusive) - ID="$2" - shift - ;; - -p|--psf) - psf="$2" - shift - ;; - -m|--mh_local) - mh_local="$2" - shift - ;; - -N|--N_SMP) - N_SMP="$2" - shift - ;; - -d|--directory) - dir="$2" - shift - ;; - -S|--scratch) - scratch="$2" - shift - ;; - -n|--dry_run) - dry_run="$2" - shift - ;; - --debug_out) - debug_out="$2" - shift - ;; - esac - shift -done - -## Check options -if [ "$job" == "-1" ]; then - echo "No job indicated, use option -j" - exit 2 -fi - -if [ "$exclusive" == "-1" ]; then - echo "No image ID indicated, use option -e" - exit 3 -fi - -if [ "$psf" != "psfex" ] && [ "$psf" != "mccd" ]; then - echo "PSF (option -p) needs to be 'psfex' or 'mccd'" - exit 4 -fi - - -source $HOME/shapepipe/scripts/sh/functions.sh - - -kind=$(get_kind_from_job $job) - - -# Load common functions -source $HOME/shapepipe/scripts/sh/functions.sh - - -# Start script - -if [ "$scratch" != "-1" ]; then - - command "mkdir -p $scratch/${kind}_runs" $dry_run - command "cp -R ${kind}_runs/$ID $scratch/${kind}_runs" $dry_run - command "cd $scratch" $dry_run - -fi - - if [ "$slurm" == "0" ]; then - command "init_run_exclusive_canfar.sh -j $job -p $psf -m $mh_local -N $N_SMP -e $ID" $dry_run - else - STATUS=$(sbatch --output=./sbatch-$ID.out --partition=comp --job-name="j${job}_${ID}" --ntasks-per-node=$N_SMP --time=32:00:00 --mem=64G $exec_path/init_run_exclusive_canfar.sh -j $job -p $psf -m $mh_local -N $N_SMP -e $ID) - - JOB_ID=$(echo $STATUS | cut -d ' ' -f 4) - echo "JOB_ID=$JOB_ID" - - # Wait for the job to finish - while true; do - STATUS=$(squeue -j "$JOB_ID" -h -o "%T") - if [[ -z "$STATUS" ]]; then - echo "job $JOB_ID no longer in the queue" - break - fi - - echo "Waiting for job $JOB_ID in state '$STATUS' to complete..." - sleep 10 - done - - echo "Job $JOB_ID has completed. Proceeding with the script..." - fi - -if [ "$scratch" != "-1" ]; then - - if [ "$job" == "32" ]; then - command "mv ${kind}_runs/$ID/output/run_sp_exp_SxSe* $dir/${kind}_runs/$ID/output" $dry_run - elif [ "$job" == "64" ]; then - command "mv ${kind}_runs/$ID/output/run_sp_tile_PsViSm* $dir/${kind}_runs/$ID/output" $dry_run - elif [ "$job" == "128" ]; then - command "mv ${kind}_runs/$ID/output/run_sp_tile_ngmix_* $dir/${kind}_runs/$ID/output" $dry_run - fi - - command "rm -rf ${kind}_runs/$ID" $dry_run - command "cd $dir/${kind}_runs/$ID" $dry_run - # Gave Input/Output python error - #command "update_runs_log_file.py" $dry_run - command "cd $dir" $dry_run - -fi diff --git a/scripts/sh/stats_jobs_canfar.sh b/scripts/sh/stats_jobs_canfar.sh deleted file mode 100755 index f9c51101a..000000000 --- a/scripts/sh/stats_jobs_canfar.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash - -# Name: stats_jobs_canfar.sh -# Author: Martin Kilbinger -# Description: Handles headless jobs on canfar - - -# Global variables - -## Temporary files -tmpfile_jobs="jobinfo.txt" -tmpfile_ids="ids.txt" -tmpfile_running="jobs_running.txt" - -## curl options -SSL=~/.ssl/cadcproxy.pem -SESSION=https://ws-uv.canfar.net/skaha/v0/session - - -# Command line arguments - -## Default values -mode="count" -debug=0 - -## Help string -usage="Usage: $(basename "$0") [OPTIONS] -\n\nOptions:\n - -h\tthis message\n - -m, --mode MODE\n - \tmode, allowed are 'count' (default), 'delete'\n - -d, --debug\n -" - -## Parse command line -while [ $# -gt 0 ]; do - case "$1" in - -h) - echo -ne $usage - exit 0 - ;; - -m|--mode) - mode="$2" - shift - ;; - -d|--debug) - debug=1 - ;; - esac - shift -done - -## Check options -case $mode in - "count"|"delete") - # valid option - ;; - *) - echo "Invalid mode $mode" - exit 1 - ;; -esac - - -# Main program - -# Get all instances -if [ "$debug" == "1" ]; then - curl -E $SSL $SESSION - echo curl -E $SSL $SESSION - exit 0 -else - curl -E $SSL $SESSION &> /dev/null > $tmpfile_jobs -fi -res=$? - -if [ "$res" == "0" ]; then - - # Get headless job IDs - cat $tmpfile_jobs | grep headless -B 4 -A 2 | grep Running -A 1 > $tmpfile_ids - - # Number of jobs - n_headless=`cat $tmpfile_ids | grep Running | wc -l` - - # Get running job info - cat $tmpfile_ids | grep name | perl -F\- -ane 'chomp; $F[4] =~ s/[",]//g; print "$F[3].$F[4]"' > $tmpfile_running - -else - - # Failure: set to very high number - n_headless=10000 - -fi - - -if [ "$mode" == "count" ]; then - - echo $n_headless - -elif [ "$mode" == "delete" ]; then - - echo -n "Delete $n_headless jobs? [y|n] " - read answer - if [ "$answer" == "y" ]; then - cat $tmpfile_jobs | grep headless -B 34 -A 6 | grep Running -A 34 | grep id | grep -v user | perl -F\" -ane 'print "$F[3]\n"' > $tmpfile_ids - for ID in `cat $tmpfile_ids`; do - echo $ID - # Delete headless jobs - echo "curl -X DELETE -E $SSL $SESSION/$ID" - curl -X DELETE -E $SSL $SESSION/$ID - echo $? - done - fi - -fi - - -# Remove temporary files -#rm -f $tmpfile_jobs $tmpfile_ids diff --git a/src/shapepipe/canfar/canfar_submit.py b/src/shapepipe/canfar/canfar_submit.py deleted file mode 100644 index 685584b99..000000000 --- a/src/shapepipe/canfar/canfar_submit.py +++ /dev/null @@ -1,576 +0,0 @@ -# canfar_submit_job.py - -# Submit job with the canfar client - -import os -import sys -import asyncio -import math - -from canfar.sessions import Session -from canfar.sessions import AsyncSession -from datetime import datetime - -from cs_util import args as cs_args -from cs_util import logging - - -class Job(object): - """Class Job. - - Handles job submission with the canfar system. - - """ - - def __init__(self): - - self.params_default() - - self._patch = os.environ["patch"] if "patch" in os.environ else "P0" - - # Maximum replicas per batch (CANFAR limit is 512) - self._max_replicas_per_batch = 512 - if self._max_replicas_per_batch != 512: - print(f"Setting the maximum number of replicas to the non-standard {self._max_replicas_per_batch}") - - # Default number of cores, can be overwritten by -c or -P (parallel jobs) options - self._cores_default = 2 - - - def set_params_from_command_line(self, args): - """Set Params From Command line. - - Only use when calling using python from command line. - Does not work from ipython or jupyter. - - """ - # Read command line options - options = cs_args.parse_options( - self._params, - self._short_options, - self._types, - self._help_strings, - ) - self._params = options - - # Save calling command - logging.log_command(args) - - def params_default(self): - """Params Default. - - Set default parameters. - - """ - self._params = { - "job": -1, - "exclusive": None, - "file_IDs": None, - "psf": "psfex", - "n": "0", - "debug_out": None, - "dry_run": 0, - "log": "log_jobs.txt", - "sync": "async", - "cores": 2, - "ram": 4, - "parallel_jobs": 1, - "jobs_per_session": 0, - "stats": False, - "version": "1.1", - } - - self._short_options = { - "job": "-j", - "exclusive": "-e", - "file_IDs": "-f", - "psf": "-p", - "dry_run": "-n", - "log": "-l", - "sync": "-S", - "cores": "-c", - "ram": "-r", - "parallel_jobs": "-P", - "jobs_per_session": "-J", - "stats": "-s", - "version": "-V", - } - - self._types = { - "job": "int", - "cores": "int", - "ram": "int", - "parallel_jobs": "int", - "jobs_per_session": "int", - "stats": "bool", - } - - self._help_strings = { - "job": "Running JOB, bit-coded, default is {}", - "exclusive": "Run exclusively given image", - "file_IDs": "file containing IDs", - "psf": "PSF model, allowed are 'psfex' and 'mccd'; default is {}", - "dry_run": "if dry run > 0 no actual processing; allowed are 2, 1, 0; default is {}", - "debug_out": "debug output file path, default:set automatically", - "log": "output log file with job and tile IDs", - "sync": "job submission mode, allowed are async, sync; default is {}", - "cores": "number of CPU cores per replica; default is {} or overwritten by -P", - "ram": "RAM in GB per replica; default is {}", - "parallel_jobs": "number of tiles to process in parallel per replica; default is {}", - "jobs_per_session": ( - "number of jobs (tiles) per session; if 0, create one session per job (default is {});" - + " Number of replicas is #FILE_IDs / JOBS_PER_SESSION" - ), - "stats": "print stats of sessions/batches/replicas and exit", - "version": "shapepipe image version; default is {}", - } - - def update_params(self): - """Update Params. - - Update parameters. - """ - - # Set version and image - version = self._params["version"] - self._image = f"images.canfar.net/unions/shapepipe:{version}" - - # If not given, set number of cores to number of parallel jobs; - # else set to default. - if self._params["cores"] == -1: - if self._params["parallel_jobs"] != 1: - self._params["cores"] = self._params["parallel_jobs"] - else: - self._params["cores"] = self._cores_default - - def check_params(self): - """Check Params. - - Check command line arguments and parameters. - - Returns - ------- - bool - status of checks; True (False) if passed (failed) - - """ - if self._params["job"] == -1: - raise ValueError(f"need to specify job (option -j)") - - if self._params["sync"] == "async" and not self._params["file_IDs"]: - raise ValueError("asynchronous mode only possible when tile_IDs file given (-f)") - - if not self._params["exclusive"] and not self._params["file_IDs"]: - raise ValueError("No image ID(s) indicated, use option -e ID or -f file_IDs") - - def set_options_base(self): - """Set Options Base. - - Set basic options from command line options and parameters. - - """ - opt = {} - - cwd = os.getcwd() - - # asynchronous mode - if self._params["sync"] == "async": - opt["-f"] = f"-f {cwd}/{self._params['file_IDs']}" - - # Options - opt["j"] = f"-j {self._params['job']}" - opt["p"] = f"-p {self._params['psf']}" - - # Set identifier for debug file name - if self._params["exclusive"]: - deb_ID = self._params["exclusive"] - else: - deb_ID = os.path.basename(self._params["file_IDs"]) - - if self._params['debug_out']: - opt["debug_out"] = f"--debug_out {cwd}/debug/{self._params['debug_out']}" - else: - opt["debug_out"] = f"--debug_out {cwd}/debug/debug_{deb_ID}.txt" - - opt["d"] = f"-d {cwd}" - opt["m_s"] = "-m 1 -s 1" if self._patch in ("P8", "P9") else "" - if self._params["dry_run"] != 0: - opt["n"] = f"-n {self._params['dry_run']}" - if self._params["parallel_jobs"] > 1: - opt["P"] = f"--parallel_jobs {self._params['parallel_jobs']}" - - options = "" - for key in opt: - options = f"{options} {opt[key]}" - - self._options_base = options - - def set_job_name(self, suf=None): - """Set Job Name. - - Set job name as it will appear on the canfar submission system. - - Parameters - ---------- - suf : str, optional - suffix, default is ``None`` - - """ - if suf is None: - suf = self._params["exclusive"] - - suf1 = suf.replace(".", "-") - - job_name = f"sp-{self._patch}-j{self._params['job']}-{suf1}" - - return job_name - - def set_command(self, mode): - """Set Command. - - Set shell command to run. - - Parameters - ---------- - mode: str - job submission mode, allowed are "async_single" (for asynchronous - submission of single job), "async_bulk" (asynchronous submission of many jobs), - "sync" (synchronous submission of one or more jobs) - - """ - if mode == "async_single": - self._cmd = f"{os.environ['HOME']}/shapepipe/scripts/sh/canfar_async_job.sh" - elif mode == "async_bulk": - self._cmd = f"{os.environ['HOME']}/shapepipe/scripts/python/distribute_tiles.py" - elif mode == "sync": - self._cmd = f"{os.environ['HOME']}/shapepipe/scripts/sh/init_run_exclusive_canfar.sh" - - def get_tile_IDs(self): - """Get Tile IDs. - - Return tile ID information. - - Returns - ------- - str - tile_ID(s) - int - length of tile ID list - str - suffix - - """ - if self._params["exclusive"]: - tile_IDs = [self._params["exclusive"]] - suf = None - else: - with open(self._params["file_IDs"], "r") as f: - tile_IDs = [line.strip() for line in f] - suf = "" - - return tile_IDs, len(tile_IDs), suf - - async def run_async(self): - """Run Async. - - Run asynchronous job submission. - - """ - async with AsyncSession() as session: - - _, total_n, suf = self.get_tile_IDs() - - # Calculate number of replicas (sessions) based on jobs_per_session - if self._params["jobs_per_session"] > 0: - # Each replica processes multiple jobs - num_replicas = math.ceil(total_n / self._params["jobs_per_session"]) - print(f"Distributing {total_n} jobs across {num_replicas} sessions") - print(f"Each session will process ~{self._params['jobs_per_session']} jobs") - else: - # Old behavior: one replica per job - num_replicas = total_n - print(f"Creating {num_replicas} sessions (one per job)") - - # Calculate number of batches needed - num_batches = math.ceil(num_replicas / self._max_replicas_per_batch) - - if num_batches == 1: - # Submit single batch - self.set_command(mode="async_bulk") - print(f"Submitting {num_replicas} replicas in a single batch") - return await self._submit_single_batch(num_replicas, total_n) - else: - # Submit multiple batches as chunks - self.set_command(mode="async_bulk") - print(f"Splitting into {num_batches} batches (max {self._max_replicas_per_batch} replicas per batch)") - return await self._submit_multiple_batches(num_replicas, total_n, num_batches) - - async def _submit_single_batch(self, num_replicas, total_n): - """Submit Single Batch. - - Submit a single batch of jobs. - - Parameters - ---------- - num_replicas: int - number of replicas (sessions) to create - total_n: int - total number of jobs (tiles) to process - - Returns - ------- - list - IDs of submitted sessions - """ - print(f"Submitting {num_replicas} replicas to process {total_n} jobs") - - job_name = self.set_job_name(suf="") - options = self._options_base.lstrip() - print(f"Running '{self._cmd} {options}'") - - async with AsyncSession() as session: - try: - sessions = await session.create( - name=job_name, - image=self._image, - cmd=self._cmd, - args=options, - replicas=num_replicas, - cores=self._params["cores"], - ram=self._params["ram"], - ) - print(f"✓ Batch submitted successfully: {len(sessions)} sessions created") - print(f"Each session will process ~{math.ceil(total_n / num_replicas)} jobs using chunk()") - print("Sessions = ", sessions) - except Exception as e: - print(f"❌ CANFAR session.create() failed: {type(e).__name__}: {e}") - raise - - return sessions - - async def _submit_multiple_batches(self, num_replicas, total_n, num_batches): - """Submit Multiple Batches. - - Submit multiple batches of jobs. - - Parameters - ---------- - num_replicas: int - total number of replicas (sessions) to create - total_n: int - total number of jobs (tiles) to process - num_batches: int - number of batches to split replicas into - - Each batch creates a subset of replicas. chunk() automatically - distributes all tiles across all replicas. - - Returns - ------- - list - IDs of submitted sessions - - """ - all_sessions = [] - - async with AsyncSession() as session: - for batch_num in range(1, num_batches + 1): - # Calculate batch size (number of replicas in this batch) - if batch_num < num_batches: - batch_size = self._max_replicas_per_batch - else: - # Last batch gets the remainder - batch_size = num_replicas - (num_batches - 1) * self._max_replicas_per_batch - - print(f"\n--- Batch {batch_num}/{num_batches} ---") - print(f"Submitting {batch_size} replicas") - print(f"Total {num_replicas} replicas will process {total_n} jobs") - print(f"chunk() will distribute jobs across all replicas") - - job_name = self.set_job_name(suf=f"b{batch_num}") - options = ( - f"{self._options_base} --batch_num {batch_num}" - + f" --batch_tot {num_batches}" - + f" --batch_size {batch_size}" - ) - options = options.lstrip() - print(f"Running '{self._cmd} {options}'") - - try: - sessions = await session.create( - name=job_name, - image=self._image, - cmd=self._cmd, - args=options, - replicas=batch_size, - cores=self._params["cores"], - ram=self._params["ram"], - - ) - print(f"✓ Batch {batch_num} submitted: {len(sessions)} sessions created") - print(sessions) - all_sessions.extend(sessions) - - # Small delay between batches to avoid overwhelming the system - if batch_num < num_batches: - await asyncio.sleep(1) - - except Exception as e: - print(f"❌ Batch {batch_num} failed: {type(e).__name__}: {e}") - # Continue with other batches even if one fails - continue - - print(f"\n=== Submission complete ===") - print(f"Total sessions created: {len(all_sessions)} replicas (for {total_n} jobs)") - print(f"Each replica will process ~{math.ceil(total_n / num_replicas)} jobs") - - return all_sessions - - def run_no_async(self): - """Run No Async. - - Run synchronous job submission. - - Returns - ------- - list - IDs of submitted jobs - - """ - session = Session() - - self.set_command(mode="sync") - - tile_IDs, _, _ = self.get_tile_IDs() - - job_ids = [] - for tile_ID in tile_IDs: - - job_name = self.set_job_name(tile_ID) - - options = f"{self._options_base} -e {tile_ID}" - - # Remove leading whitespace, problem in passing args below - options = options.lstrip() - - print(f"Running '{self._cmd} {options}'") - - # Submit flexible job (default - auto-scaling) - job_id = session.create( - name=job_name, - image=self._image, - cmd=self._cmd, - args=options, - ) - - print(f"Submitted job: {job_id}") - job_ids.append(job_id) - - return job_ids - - def write_job_tile_IDs(self, job_ids): - """Write Job Tile IDs - - Write tile IDs of jobs to log file. - - Parameters - ---------- - jobs_ids: list - IDs of submitted jobs. - - """ - tile_IDs, _, _ = self.get_tile_IDs() - - cwd = os.getcwd() - log_path = f"{cwd}/{self._params['log']}" - - print(f"Writing job and tile ID log file {log_path}") - with open(log_path, "w") as log: - for job_id, tile_ID in zip(job_ids, tile_IDs): - print(job_id, tile_ID, file=log) - - def print_stats(self): - """Print Stats. - - Print statistics of (to be submitted) jobs, helpfule to fine-tune parameters - before the actual submission. - - """ - # Total number of jobs (= length of input ID file) - _, total_n, _ = self.get_tile_IDs() - - if self._params["jobs_per_session"] == 0: - # Compute max jobs per session to fit in single batch - self._params["jobs_per_session"] = math.ceil(total_n / self._max_replicas_per_batch) - print(f"Max jobs per session (-J) = {self._params['jobs_per_session']}") - - # Number of sessions - n_session = math.ceil(total_n / self._params["jobs_per_session"]) - - # Number of batches - n_batch = math.ceil(n_session / self._max_replicas_per_batch) - - # Number of jobs per session - n_jobs = math.ceil(self._params["jobs_per_session"] / n_batch) - - n_jobs_serial = max( - math.ceil(n_jobs / self._params["parallel_jobs"]), - 1, - ) - - print( - f"Number of jobs = {total_n}\t\t(#{self._params['file_IDs']})") - print( - f"Number of sessions = {n_session:5d}\t\t({total_n} /" - + f" {self._params['jobs_per_session']})" - ) - print( - f"Number of batches = {n_batch:5d}\t\t({n_session} /" - + f" {self._max_replicas_per_batch})" - ) - print( - f"Number of jobs per session = {n_jobs:5d}" - + f"\t\t({self._params['jobs_per_session']} / {n_batch}" - ) - print( - f"Number of serial jobs = {n_jobs_serial:5d}\t\t({n_jobs}" - + f" / {self._params['parallel_jobs']})" - ) - - def run(self, args=None): - """Run. - - Run instance. - - Parameters - ---------- - args: list, optional - command line arguments, default is ``None`` - - """ - obj = self - - if args is None: - args = sys.argv - obj.set_params_from_command_line(args) - obj.update_params() - obj.check_params() - - obj.set_options_base() - - if self._params["stats"]: - obj.print_stats() - sys.exit(0) - - # Initialize session manager - self._session = Session() - - if obj._params["sync"] == "async": - print("Async mode") - job_ids = asyncio.run(obj.run_async()) - else: - print("Sync mode") - job_ids = obj.run_no_async() - print(f"Submitting jobs: done") - - obj.write_job_tile_IDs(job_ids) diff --git a/src/shapepipe/canfar_run.py b/src/shapepipe/canfar_run.py index 8330b8013..8f54c08ef 100644 --- a/src/shapepipe/canfar_run.py +++ b/src/shapepipe/canfar_run.py @@ -4,25 +4,7 @@ # executables are created by pyproject.toml. import sys -from shapepipe.canfar import canfar_submit, canfar_monitor, canfar_log_monitor - -def run_job(args=None): - """Run Job. - - Handles job submission with the canfar library - - Parameters - ---------- - args : list, optional - command line arguments, default is ``None`` - - """ - # Create instance - obj = canfar_submit.Job() - - # Run instance - obj.run(args=args) - +from shapepipe.canfar import canfar_monitor, canfar_log_monitor def run_log(args=None): """Run Log. diff --git a/workflow/README.md b/workflow/README.md index 1f3eee4c1..0d3a35cba 100644 --- a/workflow/README.md +++ b/workflow/README.md @@ -1,10 +1,11 @@ # ShapePipe Snakemake orchestration Snakemake workflow that orchestrates real-data ShapePipe runs. It replaces the -`curl_canfar_local.sh → run_job_sp_canfar_v2.0.bash → job_sp_canfar_v2.0.bash` -bash layers and the per-site sbatch reimplementations. **Module code is -untouched**: rules call `shapepipe_run -c ` on the existing config -chains. Design and rationale: +`run_job_sp_canfar_v2.0.bash → job_sp_canfar_v2.0.bash` bash layers, the CANFAR +submission front end that drove them (`curl_canfar_local.sh`, +`canfar_submit_job`, retired at `2ef07e45`), and the per-site sbatch +reimplementations. **Module code is untouched**: rules call +`shapepipe_run -c ` on the existing config chains. Design and rationale: [CosmoStat/shapepipe#848](https://github.com/CosmoStat/shapepipe/issues/848) (the living PRD). From 91d229c21b3c908c6c187a2942b51934dbc22be9 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:33:54 +0200 Subject: [PATCH 25/36] refactor(functions.sh): keep the one helper Gen 2 still sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit run_job_sp_canfar_v2.0.bash — still driven by sp_validation's image-sims workflow — sources this file for `command()` and nothing else. Everything around it belonged to the curl/skaha submission layer deleted in the previous commit: the CANFAR session URL, the images.canfar.net tag, `version="2.0"`, `cmd_remote`, `call_curl` (whose 10 parameters the one caller passed 14 arguments to), `update_session_logs`, `get_line_from_file` (REPLICA_ID fan-out) and `get_kind_from_job` (bit codes). `command()` is byte-identical. `pat` and `STOP` stay: `command()` reads both as free variables, and this file has always been the one that set them for run_job_sp_canfar_v2.0.bash. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- scripts/sh/functions.sh | 147 ++-------------------------------------- 1 file changed, 4 insertions(+), 143 deletions(-) diff --git a/scripts/sh/functions.sh b/scripts/sh/functions.sh index f6763c5b0..777f1a557 100644 --- a/scripts/sh/functions.sh +++ b/scripts/sh/functions.sh @@ -1,97 +1,12 @@ -# Global variables -SSL=~/.ssl/cadcproxy.pem -SESSION=https://ws-uv.canfar.net/skaha/v0/session -IMAGE=images.canfar.net/unions/shapepipe -NAME=shapepipe +# Shell helper sourced by run_job_sp_canfar_v2.0.bash. +# +# command() reads $VERBOSE, $debug_out, $pat and $STOP as free variables set +# by its caller; the two defaults below cover callers that set neither. -version="2.0" -cmd_remote="$HOME/shapepipe/scripts/sh/run_job_sp_canfar_v2.0.bash" pat="---- " STOP=0 -get_line_from_file() { - local file="" - local OPTIND opt - - # Parse options: look for -f - while getopts ":f:" opt; do - case $opt in - f) - file="$OPTARG" - ;; - \?) - echo "Unknown option: -$OPTARG", ", continuing" - #return 1 - ;; - :) - echo "Option -$OPTARG requires an argument." - return 1 - ;; - esac - done - - # Check that file and REPLICA_ID are set - if [[ -z "$file" ]]; then - echo "Error: -f option not provided." - return 1 - fi - if [[ -z "$REPLICA_ID" ]]; then - echo "Error: REPLICA_ID not set." - return 1 - fi - - # Check file exists - if [[ ! -f "$file" ]]; then - echo "Error: file '$file' not found." - return 1 - fi - - # Extract the line corresponding to REPLICA_ID (1-based index) - sed -n "${REPLICA_ID}p" "$file" -} - - - -# Add session and image IDs to log files -function update_session_logs() { - echo $my_session >> session_IDs.txt - echo "$my_session $ID" >> session_image_IDs.txt -} - -function call_curl() { - my_name=$1 - my_job=$2 - my_psf=$3 - my_ID=$4 - my_N_SMP=$5 - my_dry_run=$6 - my_dir=$7 - my_debug_out=$8 - my_scratch=$9 - my_test_arg=${10} - - my_arg="-j $my_job -p $my_psf -e $my_ID -N $my_N_SMP -n $my_dry_run -d $my_dir --debug_out $my_debug_out -S $my_scratch $my_test_arg" - - if [ "$my_dry_run" == "0" ]; then - my_session=`curl -E $SSL "$SESSION?$RESOURCES" -d "image=$IMAGE:$version" -d "name=${my_name}" -d "cmd=$cmd_remote" --data-urlencode "args=${my_arg[@]}"` - fi - - cmd=("curl" "-E" "$SSL" "$SESSION?$RESOURCES" "-d" "image=$IMAGE:$version" "-d" "name=${my_name}" "-d" "cmd=$cmd_remote" "--data-urlencode" "args=\"${my_arg}\"") - - if [ -n "$my_debug_out" ]; then - echo "${pat}call_curl $my_name $my_arg" >> $my_debug_out - echo "${pat}Running ${cmd[@]} (dry_run=$my_dry_run)" >> $my_debug_out - fi - echo "${cmd[@]} (dry_run=$my_dry_run)" - - - # Running $cmd does not work due to unknown problems with passing of args - - update_session_logs -} - - ## Print string, executes command, and prints return value. function command () { cmd=$1 @@ -142,57 +57,3 @@ function command () { fi fi } - - -function get_kind_from_job() { - my_job=$1 - - job_to_test=2 - kind="none" - - # loop over possible job numbers - while [ $job_to_test -le 1024 ]; do - - (( do_job = $job & $job_to_test )) - if [[ $do_job != 0 ]]; then - - if [ $job_to_test == 32 ]; then - if [ "$kind" == "tile" ]; then - echo "Error: Invalid job $job. mixing tile and exp kinds" - exit 6 - fi - - # job=32 -> set kind to exp - kind="exp" - elif [ $job_to_test == 2 ]; then - if [ "$kind" == "tile" ]; then - echo "Error: Invalid job $job. mixing tile and exp kinds" - exit 6 - fi - - kind="exp" - elif [ $job_to_test == 8 ]; then - if [ "$kind" == "tile" ]; then - echo "Error: Invalid job $job. mixing tile and exp kinds" - exit 6 - fi - - kind="exp" - else - if [ "$kind" == "exp" ]; then - echo "Error: Invalid job $job. mixing tile and exp kinds" - exit 6 - fi - - # job != 32 -> set kind to tile - kind="tile" - fi - - fi - - # Multiply job number by two to get next bitwise number - job_to_test=$((job_to_test * 2)) - done - - echo $kind -} From c6dac3af08eca6d5bc6fe2508e6abf1e0fa4277f Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:34:39 +0200 Subject: [PATCH 26/36] docs: retire the CANFAR production walkthrough MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pipeline_canfar.md was the v1.x production procedure end to end: the `patch=P[1-9]` loop, `-V v1.6`, the bit-coded `-j` table, and every script deleted in this PR. Its two load-bearing pieces were transplanted in Phase 0 before anything was removed — the 128/131072 healsparse pair with its "matches the bit masks" rationale, and the SGC/NGC plot windows, now in workflow/config.yaml's coverage block, plus the sp_validation consumption pointer (notebooks/demo_apply_hsp_masks.py). clusters.md keeps candide and ccin2p3 and replaces its CANFAR section with what a reader needs to know: submission is retired, orchestration is the Snakemake workflow, that workflow is SLURM-only, and the two monitor commands survive. Also fixes job_sp_canfar_v2.0.bash's header, which named the Gen 1 script deleted here rather than itself. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- docs/source/clusters.md | 39 +- docs/source/pipeline_canfar.md | 571 ----------------------------- docs/source/toc.rst | 1 - scripts/sh/job_sp_canfar_v2.0.bash | 2 +- 4 files changed, 13 insertions(+), 600 deletions(-) delete mode 100644 docs/source/pipeline_canfar.md diff --git a/docs/source/clusters.md b/docs/source/clusters.md index 41d32a0bf..521e51bb3 100644 --- a/docs/source/clusters.md +++ b/docs/source/clusters.md @@ -2,9 +2,9 @@ ShapePipe runs the same way on every cluster: **through the container**. You pull the slim `runtime` image once, bind-mount your clone, and run -`shapepipe_run` (or the CANFAR submission tooling) inside it — there is no -environment to install or activate on the host. This page covers the shared -pattern, then the specifics for each supported machine. +`shapepipe_run` inside it — there is no environment to install or activate on +the host. This page covers the shared pattern, then the specifics for each +supported machine. For what is *inside* the image and how it is built, see [Container Workflow](container.md). @@ -67,32 +67,17 @@ over unchanged. ## CANFAR -CANFAR submission does not go through a batch scheduler. Instead you submit -container jobs to CANFAR's headless system with the `canfar_submit_job` console -script (backed by the `canfar` library), and watch them with `canfar_monitor` / -`canfar_monitor_log`. Pipeline steps are **bit-coded** through `-j` (the same -scheme as `scripts/sh/job_sp_canfar.bash`), the PSF model is chosen with -`-p psfex|mccd`, and `-V` selects the image version: +CANFAR submission has been retired. The `canfar_submit_job` console script and +the bash layers underneath it (`curl_canfar_local.sh`, +`init_run_exclusive_canfar.sh`, `job_sp_canfar.bash`) were last carried at +`2ef07e45`; the bit-coded `-j` production walkthrough went with them. -```bash -# Submit pipeline step(s) for the configured tiles (bit-coded -j). -canfar_submit_job -j 1 -p psfex -V 1.1 - -# Monitor sessions/jobs and stream logs. -canfar_monitor -canfar_monitor_log -``` +Production orchestration is now the Snakemake workflow in `workflow/` — see +`workflow/README.md`. It is SLURM-only and has no CANFAR execution mode, so a +CANFAR campaign means moving to a SLURM site (nibi, candide). -The full production run — input preparation, the per-step `-j` table, and -post-processing — is documented in the -[CANFAR production walkthrough](pipeline_canfar.md). - -```{note} -The CANFAR production submission scripts (`scripts/sh/job_sp_canfar*.bash`) still -run under the pre-container environment and are slated for the same -container-first cleanup the candide scripts received. Treat the walkthrough as -the current-but-evolving production procedure. -``` +`canfar_monitor` and `canfar_monitor_log` stay: they list, filter and destroy +CANFAR sessions, independently of how those sessions were started. ## ccin2p3 diff --git a/docs/source/pipeline_canfar.md b/docs/source/pipeline_canfar.md deleted file mode 100644 index cfdd16507..000000000 --- a/docs/source/pipeline_canfar.md +++ /dev/null @@ -1,571 +0,0 @@ -# Running `ShapePipe` processing and post-processing pipelines on CANFAR - -Documentation to create ShapePipe output products for catalogues v1.x. - -## Initial Setup - -### CANFAR Login - -Login to the canfar system with - -```bash -canfar auth login -``` - -This can be done from at notebook or terminal within the canfar science portal, -or any remote terminal that has the canfar library installed. - -Check authentication status with - -```bash -canfar auth list -``` - -If not on "default", run - -```bash -canfar auth switch default -``` - -### Set variables (optional) - -Set the current patch in the shell as - -```bash -patch=P[1-9] -``` - -For convenience, the current PSF model can be set as environment variable, e.g.: - -```bash -psf="psfex" -``` - -Allowed are `psfex` and `mccd`. - -Setting the terminal title to display the patch can be useful for long jobs, to keep track of which terminal -runs which patch: - -```bash -echo -ne "\033]0;$patch\007" -``` - -### Prepare run directory - -First, go to the dedicated directory with - -```bash -cd /path/to/version/$patch -``` - -Next, set links to the tile number list and configuration directory: - -```bash -ln -s ~/shapepipe/auxdir/CFIS/tiles_202106/tiles_$patch.txt tile_numbers.txt -ln -s ~/shapepipe/example/cfis -``` - -Create output and debug log directories - -```bash -mkdir -p output -mkdir -p debug -``` - -Finally, create and link to central image storage directories for tiles and exposures: - -```bash -mkdir -p ~/cosmostat/v2/data_tiles/$patch -ln -s ~/cosmostat/v2/data_tiles/$patch data_tiles -mkdir -p ~/cosmostat/v2/data_exp/$patch -ln -s ~/cosmostat/v2/data_tiles/$patch data_exp -``` - -## `ShapePipe` processing - -Now, everything should be ready to start running `ShapePipe` for the weak lensing processing. The following -details all necessary steps. - -### Get Images - -We first download images, and in a second run create symbolic links with the proper pipeine naming scheme. - -#### Download and move tiles - -When running the main `ShapePipe` script `shapepipe_run`, the following env variable needs to point -to the current working directory - -```bash -export SP_RUN=`pwd` -``` - -Now we run the first module (`get_images_runner`) to download the tile images together with the weight files. -This run can get interrupted by VOSpace I/O or connection errors. In that case, -we move new files to the image storage directory, remove the previous (now void of images) run directory, -and update the run log file. We also check the number of previous and new tiles. - -```bash -shapepipe_run -c cfis/config_Git_vos.ini -ls -l data_tiles/ | wc -mv -i output/run_sp_Git_*/get_images_runner/output/CFIS.???.???.*fits* data_tiles -ls -l data_tiles/ | wc -rm -rf output/run_sp_Git_* -update_runs_log_file.py -``` - -Repeat the above block as needed. - -### Find Exposures - -With all tile images (= stacks) downloaded, we can inquire their headers to identify the exposures that were used -to create the stacks. This call to the pipeline also creates the symbolic links to the downloaded tile images. - -```bash -shapepipe_run -c cfis/config_GitFe_symlink.ini -``` - -(One could also run `Fe` alone.) - -### Download and Move Exposures - -The last module create exposure lists on output. These are now used to download all exposures. As for the tile downloads, -we have to account for VOSpace errors. - -```bash -shapepipe_run -c cfis/config_Gie_vos.ini -mv -i output/run_sp_Gie_*/get_images_runner/output/*.fits*fz data_exp -rm -rf output/run_sp_Gie_* -update_runs_log_file.py -``` - -Repeat the above by hand, or peform it in an automatic loop: - -```bash -while true; do - shapepipe_run -c cfis/config_Gie_vos.ini - ls -l data_exp/ | wc - mv -i output/run_sp_Gie_*/get_images_runner/output/*.fits*fz data_exp - ls -l data_exp/ | wc - rm -rf output/run_sp_Gie_* - update_runs_log_file.py -done -``` - -**Note:** Make sure that after all images are downloaded there is no `Gie` run in the output directory. -This would mess up later modules since `last:get_image_runner` could point to this run. - -### Create tile links again (necessary?) - -If necessary, e.g. because a previous `Git` run is no longer valid, re-create the symbolic links to the downloaded tiles with - -```bash -job_sp_canfar.bash -p $psf `cat tile_numbers.txt` -j 1 -r symlink -``` - -### Uncompress tile weights - -The downloaded tile weights are compressed. The following call uncompresses all. - -```bash -shapepipe_run -c cfis/config_tile_Uz.ini -``` - -### Mask tiles - -This step is done globally for all tiles. There might be job failures or interruptions. The following -command to the `ShapePipe` job script can be run repeatedly; already created masks will be skipped. - -```bash -job_sp_canfar.bash -p $psf -n $OMP_NUM_THREADS -j 4 -``` - -If masks were created in more than one run, i.e. situated in more than one output directory, these have to be -combined for subsequent pipeline module runs. This is done by creating a new output directory with symbolic -links, using the script - -```bash -combine_runs.bash -c flag_tile -``` - -## Tile detection - -We can finally run our first module using the canfar submission system. First, determine the number of optimal jobs such that at a given -time the allowed maximum of 512 running jobs is not exceeded. -Set as `N_PAR` (number of parallel jobs) a number between 1 and 8. - -```bash -canfar_submit_job -j 16 -f tile_numbers.txt -P N_PAR -v -s -``` - -Now, run the previous command with that number `JMAX` - -```bash -canfar_submit_job -j 16 -f tile_numbers.txt -P N_PAR -v -J JMAX -``` - -### Exposure Processing - -#### Option 0: Global split and exp masks (deprecated; used for earlier v1.x patch runs) - -For this option, set `sp_local=0`. - -**TODO: ** Split Uz and SpMh - -For `sp_local=-` both `mh_local` (0, 1) are ok: - -```bash -export mh_local=0 -``` - -#### Option 0: Mask exposures (deprecated) - -Run repeatedly if necessary: - -```bash -job_sp_canfar.bash -p $psf -n $OMP_NUM_THREADS -j 8 -``` - -Combine all runs: - -```bash -combine_runs.bash -c flag_exp -``` - -### Option 1: Local split and mask exposures (recommended) - -Optional: Enable flags for local split processing and merge header runs as - -```bash -export sp_local=1 -export mh_local=1 -``` - -These flags are automatically set to 1 in the new job scripts. - - -Get single-HDU single-exposure IDs file (from missing 32 job): - -```bash -summary_run P$patch 32 -cp summary/missing_job_32_all.txt exp_shdu.txt -``` - -### Split exposures - -First, determine the number of maximum jobs with the option `-s` (see above). Then, submit with - -```bash -canfar_submit_job -j 2 -v -f exp_shdu.txt -v -P N_PAR -J JMAX -``` - -### Mask exposures - -```bash -canfar_submit_job -j 8 -f exp_shdu.txt -v -P N_PAR -J JMAX -``` - -### Exposure detection - -```bash -canfar_submit_job -j 32 -f exp_shdu.txt -v -P N_PAR -J JMAX -``` - -### Tile preparation - -```bash -canfar_submit_job -j 64 -f tile_numbers.txt -``` - -### Tile shape measurement - -```bash -canfar_submit_job -j 128 -f tile_numbers.txt -``` - -### Merge sub-catalogues - -```bash -canfar_submit_job -j 256 -f tile_numbers.txt -``` - -### Create final catalogues - -```bash -canfar_submit_job -j 512 -f tile_numbers.txt -``` - -This was the last `ShapePipe` module to run for main processing. - - -### Merge all final catalogues - -The last step of `ShapePipe` processing is, per patch, to merget all final catalogues. This is done via a python script, as follows. -First, change to parent directory `/path/to/version` and run the following command for all patches - -```bash -patchnum=`tr $patch P ''` -create_final_cat.py -m final_cat_$patch.hdf5 -i . -p $patch/cfis/final_cat.param \ - -P $patchnum -o $patch/n_tiles_final.txt -v -``` - -## Additional `ShapePipe` processing - - -### Create star Catalogue - -We can additionaly create a combined star catalogue, with star shapes projecte from detector to world coordinates. -This is useful for validation and galaxy-PSF/star correlation diagnostics. - -#### Combine all PSF runs - -In each patch directory /path/to/version/$patch, run - -```bash -combine_runs.bash -p $psf -c psf -``` - -to create a single output directory of PSF files (symbolic links). - -Optionally, to create and plot results for this patch only: - -```bash -shapepipe_run -c $SP_CONFIG/config_Ms_$psf.ini -shapepipe_run -c $SP_CONFIG/config_Pl_$psf.ini -``` - -#### Collate star catalogues - -Collate all input validation PSF files into star catalogues, gathering positions -(X/Y/RA/DEC) and the MCCD CCD id. - -Note: HSM shapes are no longer rotated into world coordinates at this step. The -PSF/star ellipticities and sizes are now measured directly in sky coordinates -during PSF interpolation (galsim `FindAdaptiveMom(use_sky_coords=True)`), so -`collate_star_cat.py` only collates and passes the shapes through. - -> **v2.0 is patch-less.** Runs up to `v1.6` are organised in sky patches -> `P1`..`P`, each patch a run directory. `v2.0` removes the patch concept: a -> single run root, outputs directly under it. `v2.0` is the default; select an -> older layout with `-V` (e.g. `-V v1.6`). For `v2.0` the patch loop and the -> `-P` option no longer apply, and the patch token drops from the output -> filename (`validation_psf_conv-.fits` instead of -> `validation_psf_conv--.fits`). - -```bash -cd /path/to/version -mkdir star_cat -cd star_cat -``` - -For `v2.0` (the default), run once against the patch-less run root, producing -files `validation_psf_conv-.fits`: - -```bash -collate_star_cat.py -i .. -v -``` - -For `v1.x`, pass the version explicitly and run once per patch, creating a -directory per patch `P?` and producing files -`validation_psf_conv--.fits` (for the v1.4 setup only one file): - -```bash -collate_star_cat.py -i .. -V v1.6 -P $patchnum -v -``` - -Combine previously created files as links within one ShapePipe run directory (for the v1.4 setup only one link). -First (and optiohnal), create a subdir for a run and link to the input patches: - -```bash -cd /path/to/version/star_cat -mkdir v1.6 -ln -s ../P1 -ln -s ../P2 -... -``` - -Next, create links to all `validation_conv` runs: - -```bash -combine_runs.bash -p psfex -c psf_conv -``` - -Merge all converted star catalogues and create `final-starcat.fits`: - -```bash -export SP_RUN=`pwd` -shapepipe_run -c ~/shapepipe/example/cfis/config_Ms_psfex_conv.ini -``` - -Rename to general PSF and star catalogue used for all ("a") sub-versions: - - -```bash -cp output/run_sp_Ms/merge_starcat_runner/output/full_starcat-0000000.fits \ - unions_shapepipe_psf_2024_v1.6.a.fits -``` - -The FITS file `CATTYPE` (newer version) should be `validation_psf_conf`. - -## Post-processing - -The following post-processing steps are performed with the library `sp_validation`. - -### Extract Information - -First, we extract all information from the final catalogue, per patch. We copy -the parameter file and set links to the catalogues and `ShapePipe` config directory. - -```bash -cd /path/to/version/$patch -cp ~/astro/repositories/github/sp_validation/notebooks/params.py . -ln -s /path/to/final_cat_$patchnum.hdf5 # not relative path ../final_cat_P$patchnum.hdf5 ! -ln -s output/run_sp_MsPl/mccd_merge_starcat_runner/output/full_starcat-0000000.fits -ln -s ~/astro/repositories/github/shapepipe/example/cfis -``` - -Then edit `params.py`: Set patch name; set `wrap_ra` for P2. - -Now we can run the script, recommended via job submission on candide. For large patches, -this requies a job with a large memory, e.g. with `mem=380000` - - -```bash -[squeue] python ~/astro/repositories/github/sp_validation/notebooks/extract_info.py -``` - -This creates a patch-wise comprehensive catalogue. - -### Create global comprehensive catalogues - -```bash -cd /patch/to/version -[squeue] python ~/astro/repositories/github/sp_validation/scripts/create_joint_comprehensive_cat.py \ - -v v1.6.c -v -p P1+P2+P3+P4+P5+P6+P7+P8+P9 -``` - -This creates the file `unions_shapepipe_comprehensive_2024_v1.6.c.hdf5`. - - -### Apply structural masks - -First, edit the Python script `~/astro/repositories/github/sp_validation/notebooks/demo_apply_hsp_masks.py` -to match catalogue name. Check the coverage mask input file (see below). -Run the script to apply the healsparse structural masks: - -```bash -[squeue] python ~/astro/repositories/github/sp_validation/notebooks/demo_apply_hsp_masks.py -``` - -This creates the file `unions_shapepipe_comprehensive_struct_2024_v1.6.c.hdf5`. - - -### Define sample, calibrate catalogue - -We are close to finally perform the last post-processing step, which is the calibration. First, the final galaxy sample -in question needs to be defined, with masks and cuts to apply from a `yaml` config file. A number of pre-defined files -can be found in `~/astro/repositories/github/sp_validation/calibration`. - -For example, to create `v1.6.6`, the steps are: - -```bash -cd /path/to/version -mkdir -p v1.6.6 -cd v1.6.6 -ln -s ~/astro/repositories/github/sp_validation/calibration/mask_v1.X.6.yaml config_mask.yaml -ln -s ..//unions_shapepipe_comprehensive_struct_2024_v1.6.c.hdf5 unions_shapepipe_comprehensive_struct_2024_v1.X.c.hdf5 -[squeue] python ~/astro/repositories/github/sp_validation/calibrate_comprehensive_cat.py -``` - -calibrate_comprehensive - - -```{note} -The matched-star-catalogue diagnostic (`merge_psf_cat.py`) has moved into -[`sp_validation`](https://github.com/CosmoStat/sp_validation) / `cosmo_val`; -see that repository for its current equivalent. The coverage-mask helpers -(`get_ccds_with_psf`, `download_headers`, `extract_field_corners`, -`build_coverage_map`, `plot_coverage_map`, and `summary_run`) are shipped by -ShapePipe and documented below. -``` - -### Create matched star catalogue - -For diagnostics, a catalogue with multi-epoch shapes measured by ngmix matched with the validation star catalogue is used. -This is created as follows: - -```bash -cd /path/to/version -merge_psf_cat.py [-V v1.6|-P P1+P2+...] -v -``` - -This creates the joint catalogue unions_shapepipe_star_2024_v1.6.a.fits . - -### Create coverage mask - -First, on canfar, move to the directory that has the patch subdirectories. - -```bash -cd /path/to/version -``` - -#### Get exposure numbers - -If the file `$patch/exp_numbers.txt` does not exist for a given patch, create it with the summary program - -```bash -summary_run $patch 1 -``` - -Now, create the list of single-CCD footprints that have a valid PSF model. -Only these CCDs are stamped into the mask, so the accumulated map counts, per -sky pixel, the number of exposures with a valid PSF. The list is written to -`ccds_with_psf_v1.6.txt`: - -```bash -get_ccds_with_psf -v -V v1.6 -o ccds_with_psf_v1.6.txt -``` - -Next, download the exposure headers; indicate (with `-d`) a directory of -already downloaded headers, which are linked so duplicate downloads are -skipped: - -```bash -download_headers -i ccds_with_psf_v1.6.txt -o headers_v1.6 -d headers_v1.3 -v -``` - -From the headers, the per-CCD corner coordinates are extracted. Pass the -CCD list with `-l` so only CCDs with a valid PSF are written: - -```bash -extract_field_corners -i headers_v1.6 -l ccds_with_psf_v1.6.txt -o exp_ra_dec_v1.6.txt -v -``` - -Then, build the healsparse coverage mask file as -```bash -build_coverage_map -i exp_ra_dec_v1.6.txt -o coverage_v1.6.x.hsp -c 128 -n 131072 -v -``` - -The healsparse resolutions (128, 131072) match the bit masks. - - -Use `plot_coverage_map` to plot a region of the coverage mask. For example, -the SGC region with a colorbar clipped to the 1–5 exposure range: - -```bash -plot_coverage_map -i coverage_v1.6.x.hsp -o coverage_v1.6_SGC.png \ - -C -R -20 -r 45 -D 18 -d 40 -m 1 -M 5 -v -``` - -Here `-R`/`-r` and `-D`/`-d` set the RA and Dec plot limits, `-m`/`-M` the -colorbar range, and `-C` adds the colorbar. Building and plotting for a range -of versions (SGC and NGC regions) is automated by -`build_and_plot_coverage_maps.sh`. - - -## Extra Utilities - -### Run in Terminal in Parallel - -```bash -cat IDs.txt | xargs -I {} -P 16 bash -c 'init_run_exclusive_canfar.sh -j 512 -e {}' -``` diff --git a/docs/source/toc.rst b/docs/source/toc.rst index 9312ac09a..4faa9f3b9 100644 --- a/docs/source/toc.rst +++ b/docs/source/toc.rst @@ -33,7 +33,6 @@ :caption: Running on a cluster clusters - pipeline_canfar .. toctree:: :hidden: diff --git a/scripts/sh/job_sp_canfar_v2.0.bash b/scripts/sh/job_sp_canfar_v2.0.bash index 592cae70d..e0fb28a01 100755 --- a/scripts/sh/job_sp_canfar_v2.0.bash +++ b/scripts/sh/job_sp_canfar_v2.0.bash @@ -1,6 +1,6 @@ #!/bin/bash -# Name: job_sp_canfar.bash +# Name: job_sp_canfar_v2.0.bash # Description: General script to process one or more tiles # with all contributing exposures. # This works as job submission script for From 05a097f6b63fbed44a72b4201b3909148018f79b Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:35:12 +0200 Subject: [PATCH 27/36] docs: say what the canfar dependency is still for MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both lines described a capability this PR removed. The `canfar` package stays, but its only importers are now canfar_monitor and canfar_log_monitor — session monitoring, not job submission — and clusters.md no longer carries a worked CANFAR example. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- README.rst | 2 +- docs/source/dependencies.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e27b4028d..40999f6fd 100644 --- a/README.rst +++ b/README.rst @@ -50,7 +50,7 @@ to start: - `Installation `_ — getting ShapePipe onto your machine or cluster. - `Basic execution `_ and `configuration `_ — running ``shapepipe_run`` and writing pipeline configs. - `Container workflow `_ — the two image targets and the ``pyproject.toml`` / ``uv.lock`` / ``Dockerfile`` layers. -- `Running on a cluster `_ — pulling the image and submitting jobs, with worked candide (SLURM) and CANFAR examples. +- `Running on a cluster `_ — pulling the image and submitting jobs, with a worked candide (SLURM) example. If you use ShapePipe in academic work, please cite Guinot et al. (2022) and Farrens et al. (2022). diff --git a/docs/source/dependencies.md b/docs/source/dependencies.md index bf595346b..2a44d9700 100644 --- a/docs/source/dependencies.md +++ b/docs/source/dependencies.md @@ -35,7 +35,7 @@ Data access & infrastructure (CANFAR / UNIONS): | Package | Purpose | |---------|---------| | [vos](https://github.com/opencadc/vostools) | CADC / CANFAR VOSpace access | -| canfar | CANFAR container-job submission | +| canfar | CANFAR session monitoring | | [astroquery](https://astroquery.readthedocs.io/) | external catalogue queries | | [cs_util](https://github.com/CosmoStat/cs_util) | shared CosmoStat utilities | | [sqlitedict](https://github.com/RaRe-Technologies/sqlitedict) | on-disk pipeline state | From e8079d8745e46ada6952331bd34ca8df7819f888 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:43:15 +0200 Subject: [PATCH 28/36] refactor(collate_star_cat): keep the psfex, patch-less path and nothing else The script had three axes of branching on top of one collation: the catalogue version (-V, with the v1.3..v1.6 patch table), the sky patch (-P, the P loop and the patch token in the output filename), and the PSF model (-p, psfex or mccd). Only one combination is used and only one is buildable: -V v2.0 -p psfex, i.e. patch-less input under /output and psfex_interp outputs. Deleted, in order of how dead they were: - version_cat/-V, allowed_versions, patches/-P, collate_paths(), the patch loop, the mode=="test" P3/P4 patch list, and the patch parameter of output_filename()/transform_exposures(). Names lose the patch token they already lacked at -V v2.0: validation_psf_conv-.fits, still what example/cfis/config_Ms_psfex_conv.ini's FILE_PATTERN consumes. - the psf=="mccd" branch. It is unreachable in practice and broken where it is reachable: no workflow config names an mccd runner, and its header path concatenated "setools_runner/output/mask" with "validation_psf" without a separator, a failure the bare `except: continue` swallowed silently. With it go transform_shape() (its only user, and hence the galsim import), the vendored Loc2Glob/Glob2CCD focal-plane geometry (its only users), _dt_mccd and sub_dir_setools. transform_shape's math is not lost: tests/module/test_hsm_sky_coords.py already carries it verbatim as the reference the world-coordinate HSM measurement is checked against. - update_params(), whose whole body was the psf switch. The two surviving paths are module constants (EXP_RUN_GLOB, PSF_INTERP_SUBDIR). tests/module/test_collate_star_cat.py goes in the same commit: every test in it pinned patch versus patch-less, a distinction that no longer exists. The module docstring gains the caveat this strip must not be read as closing: no workflow rule produces validation_psf_conv-*.fits. The input layout here is still a link farm of psfex_interp run directories, while the workflow persists validation_psf--.fits into the exp_persist tars. Repointing the script at those is a follow-up. 895 -> 301 lines, with the psfex collation block byte-identical modulo indentation. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- scripts/python/collate_star_cat.py | 777 +++----------------------- tests/module/test_collate_star_cat.py | 70 --- 2 files changed, 93 insertions(+), 754 deletions(-) delete mode 100644 tests/module/test_collate_star_cat.py diff --git a/scripts/python/collate_star_cat.py b/scripts/python/collate_star_cat.py index f7ab25b41..a3ccc2a2e 100755 --- a/scripts/python/collate_star_cat.py +++ b/scripts/python/collate_star_cat.py @@ -2,29 +2,25 @@ """COLLATE STAR CATALOGUES. -Collate the per-exposure PSF validation catalogues into star catalogues: gather -positions (X/Y/RA/DEC), assign the MCCD focal-plane CCD id, and merge into -``validation_psf_conv`` FITS files. - -Catalogue layout depends on the major version (``-V``). Runs up to ``v1.6`` are -organised in sky patches (``P1``..``P``): input runs live under -``/P/output`` and outputs are named -``validation_psf_conv--.fits``. ``v2.0`` removes the patch concept: -input runs live under a single ``/output`` root and outputs drop the patch -token (``validation_psf_conv-.fits``). - -HSM ellipticities and sizes are no longer rotated here. PSFEx and the in-repo -MCCD interpolation now measure adaptive moments directly in world coordinates -(galsim ``FindAdaptiveMom(use_sky_coords=True)``), so the WCS-Jacobian shape -rotation this script used to perform is redundant and has been removed. - -Caveat: the MCCD ``PSF_MOM_LIST``/``STAR_MOM_LIST`` columns are produced by the -external ``mccd`` fit-validation code (``mccd.auxiliary_fun.mccd_validation``), -which still measures HSM moments in the pixel frame. Those shapes are therefore -still rotated into world coordinates here, via the WCS-Jacobian rotation, until -``mccd`` itself adopts ``use_sky_coords``. This is the one branch that keeps the -rotation; the in-repo PSFEx and MCCD-interpolation paths measure adaptive -moments directly in world coordinates upstream and pass them straight through. +Collate the per-exposure PSFEx validation catalogues into star catalogues: +gather positions (X/Y/RA/DEC) and the HSM PSF and star shapes, tag each row +with its CCD number, and merge one ``validation_psf_conv-.fits`` per +single-exposure run. + +HSM ellipticities and sizes are not rotated here. ``psfex_interp`` measures +adaptive moments directly in world coordinates (galsim +``FindAdaptiveMom(use_sky_coords=True)``), so the WCS-Jacobian shape rotation +this script used to perform is redundant and has been removed; only positions +are collated. + +Caveat -- no workflow rule produces these catalogues yet. The input layout +below is the pre-workflow one: a link farm of ``psfex_interp`` run directories +under a single input root. The Snakemake workflow instead persists +``validation_psf--.fits`` into the per-exposure ``exp_persist`` +tars on the products root, and ``merge_starcat_runner`` still wants the +collated ``validation_psf_conv-*`` files for the rho/tau statistics. +Repointing this script at the persist tars (or replacing it with a rule) is a +follow-up, and a bigger change than this strip. """ import sys @@ -37,51 +33,27 @@ import numpy as np from astropy.io import fits -import galsim from cs_util import args as cs_args from cs_util import logging -def collate_paths(input_base_dir, output_base_dir, patch): - """Collate Paths. +# Input layout: single-exposure run directories matching ``EXP_RUN_GLOB`` under +# ``/output``, each holding the PSF interpolation output in +# ``PSF_INTERP_SUBDIR``. +EXP_RUN_GLOB = "run_sp_combined_psf*" +PSF_INTERP_SUBDIR = "psfex_interp_runner/output" - Return the ``(input run dir, output dir)`` for a patch. ``patch`` is None - for the patch-less v2.0 layout, which drops the ``P`` token; v1.x - passes the patch number. - Parameters - ---------- - input_base_dir : str - input base directory - output_base_dir : str - output base directory - patch : str or None - patch number, or None for the patch-less v2.0 layout - - Returns - ------- - tuple - input run directory and output directory - - """ - if patch is None: - return f"{input_base_dir}/output/", output_base_dir - return f"{input_base_dir}/P{patch}/output/", f"{output_base_dir}/P{patch}" - - -def output_filename(file_pattern, patch, idx): +def output_filename(file_pattern, idx): """Output Filename. - Build the collated catalogue filename. ``patch`` is None for the patch-less - v2.0 layout, which drops the patch token. + Build the collated catalogue filename. Parameters ---------- file_pattern : str input file pattern (e.g. ``validation_psf``) - patch : str or None - patch number, or None for the patch-less v2.0 layout idx : int exposure run index @@ -91,340 +63,7 @@ def output_filename(file_pattern, patch, idx): output catalogue file name """ - patch_token = "" if patch is None else f"{patch}-" - return f"{file_pattern}_conv-{patch_token}{idx}.fits" - - -def transform_shape(mom_list, jac): - """Transform Shape. - - Transform shape (ellipticity and size) using a Jacobian. - - Parameters - ---------- - mom_list : list - input moment measurements; each list element contains - first and second ellipticity component and size - jac : galsim.JacobianWCS - Jacobian transformation matrix information - - Returns - ------- - list - transformed shape parameters, which are - first and second ellipticity component and size - - """ - scale, shear, theta, flip = jac.getDecomposition() - - sig_tmp = mom_list[2] * scale - shape = galsim.Shear(g1=mom_list[0], g2=mom_list[1]) - if flip: - # The following output is not observed - print("FLIP!") - shape = galsim.Shear(g1=-shape.g1, g2=shape.g2) - shape = galsim.Shear(g=shape.g, beta=shape.beta + theta) - shape = shear + shape - - return shape.g1, shape.g2, sig_tmp - - -class Loc2Glob(object): - r"""Change from local to global coordinates. - - Class to pass from local coordinates to global coordinates under - CFIS (CFHT) MegaCam instrument. The geometrical informcation of the - instrument is encoded in this function. - - Parameters - ---------- - x_gap : int - Gap between the CCDs along the horizontal direction; - default is ``70`` (MegaCam value) - y_gap : int - Gap between the CCDs along the vertical direction; - Default is ``425`` (MegaCam value) - x_npix : int - Number of pixels per CCD along the horizontal direction; - default is ``2048`` (MegaCam value) - y_npix : int - Number of pixels per CCD along the vertical direction; - default to ``4612`` (MegaCam value) - ccd_tot : int - Total number of CCDs; - default to ``40`` (MegaCam value) - - Notes - ----- - This is the geometry of MegaCam. Watch out with the conventions ba,ab that means where - is the local coordinate system origin for each CCD. - For more info check out MegaCam's instrument webpage. - - Examples - -------- - 'COMMENT (North on top, East to the left)', - 'COMMENT --------------------------', - 'COMMENT ba ba ba ba ba ba ba ba ba', - 'COMMENT 00 01 02 03 04 05 06 07 08', - 'COMMENT --------------------------------', - 'COMMENT ba ba ba ba ba ba ba ba ba ba ba', - 'COMMENT 36 09 10 11 12 13 14 15 16 17 37', - 'COMMENT --------------*-----------------', - 'COMMENT 38 18 19 20 21 22 23 24 25 26 39', - 'COMMENT ab ab ab ab ab ab ab ab ab ab ab', - 'COMMENT --------------------------------', - 'COMMENT 27 28 29 30 31 32 33 34 35', - 'COMMENT ab ab ab ab ab ab ab ab ab', - 'COMMENT __________________________' - """ - - def __init__( - self, x_gap=70, y_gap=425, x_npix=2048, y_npix=4612, ccd_tot=40 - ): - r"""Initialize with instrument geometry.""" - self.x_gap = x_gap - self.y_gap = y_gap - self.x_npix = x_npix - self.y_npix = y_npix - self.ccd_tot = ccd_tot - - def loc2glob_img_coord(self, ccd_n, x_coor, y_coor): - """loc2glob Img Coord. - - Go from the local to the global img (pixel) coordinate system. - - Global system with (0,0) in the intersection of ccds [12,13,21,22]. - - Parameters - ---------- - ccd_n: int - CCD number of the considered positions - x_coor: float - Local coordinate system hotizontal value - y_coor: float - Local coordinate system vertical value - - Returns - ------- - glob_x_coor: float - Horizontal position in global coordinate system - glob_y_coor: float - Vertical position in global coordinate system - - """ - # Flip axes - x_coor, y_coor = self.flip_coord(ccd_n, x_coor, y_coor) - - # Calculate the shift - x_shift, y_shift = self.shift_coord(ccd_n) - - # Return new coordinates - return x_coor + x_shift, y_coor + y_shift - - def flip_coord(self, ccd_n, x_coor, y_coor): - r"""Change of coordinate convention. - - So that all of them are coherent on the global coordinate system. - So that the origin is on the south-west corner. - Positive: South to North ; West to East. - """ - if ccd_n < 18 or ccd_n in [36, 37]: - x_coor = self.x_npix - x_coor + 1 - y_coor = self.y_npix - y_coor + 1 - else: - pass - - return x_coor, y_coor - - def x_coord_range(self): - r"""Return range of the x coordinate.""" - max_x = self.x_npix * 6 + self.x_gap * 5 - min_x = self.x_npix * (-5) + self.x_gap * (-5) - return min_x, max_x - - def y_coord_range(self): - r"""Return range of the y coordinate.""" - max_y = self.y_npix * 2 + self.y_gap * 1 - min_y = self.y_npix * (-2) + self.y_gap * (-2) - return min_y, max_y - - def shift_coord(self, ccd_n): - r"""Provide the shifting. - - It is needed to go from the local coordinate - system origin to the global coordinate system origin. - """ - if ccd_n < 9: - # first row - x_shift = (ccd_n - 4) * (self.x_gap + self.x_npix) - y_shift = self.y_gap + self.y_npix - return x_shift, y_shift - - elif ccd_n < 18: - # second row, non-ears - x_shift = (ccd_n - 13) * (self.x_gap + self.x_npix) - y_shift = 0.0 - return x_shift, y_shift - - elif ccd_n < 27: - # third row non-ears - x_shift = (ccd_n - 22) * (self.x_gap + self.x_npix) - y_shift = -1.0 * (self.y_gap + self.y_npix) - return x_shift, y_shift - - elif ccd_n < 36: - # fourth row - x_shift = (ccd_n - 31) * (self.x_gap + self.x_npix) - y_shift = -2.0 * (self.y_gap + self.y_npix) - return x_shift, y_shift - - elif ccd_n < 37: - # ccd= 36 ears, second row - x_shift = (-5.0) * (self.x_gap + self.x_npix) - y_shift = 0.0 - return x_shift, y_shift - - elif ccd_n < 38: - # ccd= 37 ears, second row - x_shift = 5.0 * (self.x_gap + self.x_npix) - y_shift = 0.0 - return x_shift, y_shift - - elif ccd_n < 39: - # ccd= 38 ears, third row - x_shift = (-5.0) * (self.x_gap + self.x_npix) - y_shift = -1.0 * (self.y_gap + self.y_npix) - return x_shift, y_shift - - elif ccd_n < 40: - # ccd= 39 ears, third row - x_shift = 5.0 * (self.x_gap + self.x_npix) - y_shift = -1.0 * (self.y_gap + self.y_npix) - return x_shift, y_shift - - -class Glob2CCD(object): - r"""Get the CCD ID number from the global coordinate position. - - The Loc2Glob() object as input is the one that defines the instrument's - geometry. - - Parameters - ---------- - loc2glob: Loc2Glob object - Object with the desired focal plane geometry. - with_gaps: bool - If add the gaps to the CCD area. - """ - - def __init__(self, loc2glob, with_gaps=True): - # Save loc2glob object - self.loc2glob = loc2glob - self.with_gaps = with_gaps - self.ccd_list = np.arange(self.loc2glob.ccd_tot) - # Init edges defininf the CCDs - self.edge_x_list, self.edge_y_list = self.build_all_edges() - - def build_all_edges(self): - """Build the edges for all the CCDs in the focal plane.""" - edge_xy_list = [] - for idx in (0, 1): - edge_list = np.array( - [self.build_edge(ccd_n)[idx] for ccd_n in self.ccd_list] - ) - edge_xy_list.append(edge_list) - - return edge_xy_list - - def build_edge(self, ccd_n): - """Build the edges of the `ccd_n` in global coordinates.""" - if self.with_gaps: - corners = np.array( - [ - [-self.loc2glob.x_gap / 2, -self.loc2glob.y_gap / 2], - [ - self.loc2glob.x_npix + self.loc2glob.x_gap / 2, - -self.loc2glob.y_gap / 2, - ], - [ - -self.loc2glob.x_gap / 2, - self.loc2glob.y_npix + self.loc2glob.y_gap / 2, - ], - [ - self.loc2glob.x_npix + self.loc2glob.x_gap / 2, - self.loc2glob.y_npix + self.loc2glob.y_gap / 2, - ], - ] - ) - else: - corners = np.array( - [ - [0, 0], - [self.loc2glob.x_npix, 0], - [0, self.loc2glob.y_npix], - [self.loc2glob.x_npix, self.loc2glob.y_npix], - ] - ) - - glob_corners = np.array( - [ - self.loc2glob.loc2glob_img_coord(ccd_n, pos[0], pos[1]) - for pos in corners - ] - ) - - edge_xy = [] - for idx in (0, 1): - edge = np.array( - [np.min(glob_corners[:, idx]), np.max(glob_corners[:, idx])] - ) - edge_xy.append(edge) - - return edge_xy - - def is_inside(self, x, y, edge_x, edge_y): - """Is the position inside the edges. - - Return True if the position is within the rectangle - defined by the edges. - - Parameters - ---------- - x: float - Horizontal position in global coordinate system. - y: float - Vertical position in global coordinate system. - edge_x: np.ndarray - Edge defined as `np.array([min_x, max_x])`. - edge_y: np.ndarray - Edge defined as `np.array([min_y, max_y])`. - """ - if ( - (x > edge_x[0]) - and (x < edge_x[1]) - and (y > edge_y[0]) - and (y < edge_y[1]) - ): - return True - else: - return False - - def get_ccd_n(self, x, y): - """Returns the CCD number from the position `(x, y)`. - - Returns `None` if the position is not found. - """ - bool_list = np.array( - [ - self.is_inside(x, y, edge_x, edge_y) - for edge_x, edge_y in zip(self.edge_x_list, self.edge_y_list) - ] - ) - - try: - return self.ccd_list[bool_list][0] - except Exception: - return None + return f"{file_pattern}_conv-{idx}.fits" class Convert(object): @@ -457,38 +96,25 @@ def params_default(self): self._params = { "input_base_dir": ".", "output_base_dir": ".", - "version_cat": "v2.0", "mode": "merge", - "patches": "", - "psf": "psfex", "file_pattern_psfint": "validation_psf", } self._short_options = { "input_base_dir": "-i", - "version_cat": "-V", "mode": "-m", - "psf": "-p", - "patches": "-P", } self._types = {} self._help_strings = { "input_base_dir": ( - "input base dir; for v1.x runs are expected in" - + " /P/output, for v2.0 (patch-less) in" + "input base dir; single-exposure runs are expected in" + " /output; default is {}" ), - "version_cat": ( - "catalogue major version, allowed are v1.3, v1.4, v1.5, v1.6," - + " v2.0; v2.0 is patch-less; default is {}" - ), "mode": ( "run mode, allowed are 'merge', 'test'; default is" + " '{}'" ), - "psf": "PSF model, allowed are 'psfex' and 'mccd'; default is {}", - "patches": "(list of) input patches; ignored for v2.0", } # Output column names with types @@ -508,127 +134,67 @@ def params_default(self): ("CCD_NB", int), ] - # Extra columns for MCCD:737 - self._dt_mccd = self._dt.copy() - self._dt_mccd.append(("GLOB_X", float)) - self._dt_mccd.append(("GLOB_Y", float)) - - def update_params(self): - """Update Params. - - Update parameters. - - """ - if self._params["psf"] == "psfex": - #self._params["sub_dir_pattern"] = "run_sp_exp_202" - self._params["sub_dir_pattern"] = "run_sp_combined_psf" - self._params["sub_dir_psfint"] = "psfex_interp_runner" - elif self._params["psf"] == "mccd": - self._params["sub_dir_pattern"] = "run_sp_exp_SxSePsf_202" - self._params["sub_dir_psfint"] = "mccd_fit_val_runner" - self._params["sub_dir_setools"] = "setools_runner/output/mask" - else: - raise ValueError(f"Invalid PSF model {self._params['psf']}") - self._params["sub_dir_psfint"] = ( - f"{self._params['sub_dir_psfint']}/output" - ) - def run(self): """Run. Main processing function. """ - # Guard against a mistyped version silently falling through to the - # v1.x patch loop (e.g. ``-V v2`` or ``-V 2.0``). - allowed_versions = ("v1.3", "v1.4", "v1.5", "v1.6", "v2.0") - if self._params["version_cat"] not in allowed_versions: - raise ValueError( - f"Invalid version {self._params['version_cat']}; allowed are" - + f" {', '.join(allowed_versions)}" - ) - - # v2.0 removes the patch concept: a single patch-less run root. For - # v1.x, iterate over the requested sky patches as before. ``patch`` is - # None in the patch-less case, which drops the patch token from the - # input path and the output filename. - if self._params["version_cat"] == "v2.0": - patch_nums = [None] - elif self._params["mode"] == "test": - patch_nums = ["3", "4"] - else: - patch_nums = cs_args.my_string_split(self._params["patches"]) - do_parallel = True - # Loop over patches - for patch in patch_nums: + output_dir = self._params["output_base_dir"] + if not os.path.isdir(output_dir): + os.makedirs(output_dir, exist_ok=True) - patch_dir, output_dir = collate_paths( - self._params["input_base_dir"], - self._params["output_base_dir"], - patch, - ) - print("Running patch-less (v2.0)" if patch is None else f"Running patch: {patch}") - - if not os.path.isdir(output_dir): - os.makedirs(output_dir, exist_ok=True) + subdirs = f"{self._params['input_base_dir']}/output/{EXP_RUN_GLOB}" + exp_run_dirs = glob.glob(subdirs) + n_exp_runs = len(exp_run_dirs) + print(f"Found {n_exp_runs} input single-exposure run(s) ({subdirs})") - subdirs = f"{patch_dir}/{self._params['sub_dir_pattern']}*" - exp_run_dirs = glob.glob(subdirs) + if self._params["mode"] == "test": + exp_run_dirs = exp_run_dirs[:2] n_exp_runs = len(exp_run_dirs) print( - f"Found {n_exp_runs} input single-exposure run(s) for patch" - + f" {patch_dir} ({subdirs})" + f"test mode: only using {n_exp_runs} input single-exposure" + + f" runs" ) - if self._params["mode"] == "test": - exp_run_dirs = exp_run_dirs[:2] - n_exp_runs = len(exp_run_dirs) - print( - f"test mode: only using {n_exp_runs} input single-exposure" - + f" runs" + # Loop over exposure runs + if not do_parallel: + for idx_exp, exp_run_dir in tqdm( + enumerate(exp_run_dirs), + total=n_exp_runs, + disable=self._params["verbose"], + ): + self.transform_exposures(output_dir, idx_exp, exp_run_dir) + else: + res = Parallel(n_jobs=-1, backend="loky")( + delayed(self.transform_exposures)( + output_dir, idx_exp, exp_run_dir ) - - # Loop over exposure runs - if not do_parallel: for idx_exp, exp_run_dir in tqdm( enumerate(exp_run_dirs), total=n_exp_runs, disable=self._params["verbose"], - ): - self.transform_exposures( - output_dir, patch, idx_exp, exp_run_dir - ) - else: - res = Parallel(n_jobs=-1, backend="loky")( - delayed(self.transform_exposures)( - output_dir, patch, idx_exp, exp_run_dir - ) - for idx_exp, exp_run_dir in tqdm( - enumerate(exp_run_dirs), - total=n_exp_runs, - disable=self._params["verbose"], - ) ) + ) - def transform_exposures(self, output_dir, patch, idx, exp_run_dir): - """Transform exposures. + def transform_exposures(self, output_dir, idx, exp_run_dir): + """Transform Exposures. - Transform shapes for exposure for a given run (input exp run dir). + Collate the PSF validation catalogues of one single-exposure run + (input exp run dir) into one output catalogue. """ output_path = ( f"{output_dir}/" - + output_filename( - self._params["file_pattern_psfint"], patch, idx - ) + + output_filename(self._params["file_pattern_psfint"], idx) ) if os.path.exists(output_path): print(f"Skipping transform_exposures, file {output_path} exists") return - psf_dir = f"{exp_run_dir}/{self._params['sub_dir_psfint']}" + psf_dir = f"{exp_run_dir}/{PSF_INTERP_SUBDIR}" try: all_files = os.listdir(psf_dir) if self._params["verbose"]: @@ -645,11 +211,7 @@ def transform_exposures(self, output_dir, patch, idx, exp_run_dir): tmp = re.findall(r"\d+", file_name) - if self._params["psf"] == "psfex": - exp_name, ccd_id = int(tmp[0]), int(tmp[1]) - elif self._params["psf"] == "mccd": - exp_name = int(tmp[0]) - ccd_id = -1 + exp_name, ccd_id = int(tmp[0]), int(tmp[1]) if self._params["verbose"]: print("Match found ", exp_name, ccd_id) @@ -657,194 +219,42 @@ def transform_exposures(self, output_dir, patch, idx, exp_run_dir): psf_file_path = f"{psf_dir}/{file_name}" try: - if self._params["psf"] == "psfex": - psf_file_hdus = fits.open(psf_file_path, memmap=False) - psf_file = psf_file_hdus[2].data - psf_file_hdus.close() - mod = "RA" - else: - psf_file = fits.getdata(psf_file_path, 1, memmap=True) - mod = "RA_LIST" + psf_file_hdus = fits.open(psf_file_path, memmap=False) + psf_file = psf_file_hdus[2].data + psf_file_hdus.close() except Exception: continue - if self._params["psf"] == "psfex": - # HSM ellipticities and sizes are measured directly in world - # coordinates upstream (FindAdaptiveMom use_sky_coords=True), so - # they are passed straight through; only positions are collated. - exp_cat = np.array( - list( - map( - tuple, - np.array( - [ - psf_file["X"], - psf_file["Y"], - psf_file["RA"], - psf_file["DEC"], - psf_file["E1_PSF_HSM"], - psf_file["E2_PSF_HSM"], - psf_file["SIGMA_PSF_HSM"], - psf_file["FLAG_PSF_HSM"], - psf_file["E1_STAR_HSM"], - psf_file["E2_STAR_HSM"], - psf_file["SIGMA_STAR_HSM"], - psf_file["FLAG_STAR_HSM"], - np.ones_like(psf_file["RA"], dtype=int) - * ccd_id, - ] - ).T.tolist(), - ) - ), - dtype=self._dt, - ) - cat_list.append(exp_cat) - - else: - l2g = Loc2Glob() - g2c = Glob2CCD(l2g) - new_ccd_id = np.array( - [ - int( - g2c.get_ccd_n( - psf_file["GLOB_POSITION_IMG_LIST"][ii, 0], - psf_file["GLOB_POSITION_IMG_LIST"][ii, 1], - ) - ) - for ii in range(len(psf_file)) - ] - ) - - # Local-to-CCD position: subtract each CCD's focal-plane shift. - new_x = np.zeros_like(psf_file[mod]) - new_y = np.zeros_like(psf_file[mod]) - - # The MCCD PSF_MOM_LIST/STAR_MOM_LIST columns come from the - # external mccd fit-validation code, which still measures HSM - # moments in the pixel frame; rotate them into world coordinates - # via the per-CCD WCS Jacobian. This rotation stays until mccd - # itself adopts use_sky_coords (see the module docstring). The - # in-repo PSFEx / MCCD-interpolation paths are already in world - # coordinates and are passed through unrotated. - new_e1_psf = np.zeros_like(psf_file[mod]) - new_e2_psf = np.zeros_like(psf_file[mod]) - new_sig_psf = np.zeros_like(psf_file[mod]) - new_e1_star = np.zeros_like(psf_file[mod]) - new_e2_star = np.zeros_like(psf_file[mod]) - new_sig_star = np.zeros_like(psf_file[mod]) - new_flag_psf = np.zeros_like(psf_file[mod]) - new_flag_star = np.zeros_like(psf_file[mod]) - for ccd_id in range(40): - m_ccd_id = new_ccd_id == ccd_id - if sum(m_ccd_id) == 0: - continue - - x_shift, y_shift = l2g.shift_coord(ccd_id) - - new_x[m_ccd_id] = ( - psf_file["GLOB_POSITION_IMG_LIST"][:, 0][m_ccd_id] - - x_shift + # HSM ellipticities and sizes are measured directly in world + # coordinates upstream (FindAdaptiveMom use_sky_coords=True), so + # they are passed straight through; only positions are collated. + exp_cat = np.array( + list( + map( + tuple, + np.array( + [ + psf_file["X"], + psf_file["Y"], + psf_file["RA"], + psf_file["DEC"], + psf_file["E1_PSF_HSM"], + psf_file["E2_PSF_HSM"], + psf_file["SIGMA_PSF_HSM"], + psf_file["FLAG_PSF_HSM"], + psf_file["E1_STAR_HSM"], + psf_file["E2_STAR_HSM"], + psf_file["SIGMA_STAR_HSM"], + psf_file["FLAG_STAR_HSM"], + np.ones_like(psf_file["RA"], dtype=int) + * ccd_id, + ] + ).T.tolist(), ) - new_y[m_ccd_id] = ( - psf_file["GLOB_POSITION_IMG_LIST"][:, 1][m_ccd_id] - - y_shift - ) - - header_file_path = ( - self._params["sub_dir_setools"] - + self._params["file_pattern_psfint"] - + f"{exp_name}-{ccd_id}.fits" - ) - try: - header_file = fits.getdata(header_file_path, 1) - except Exception: - continue - header = fits.Header.fromstring( - "\n".join(header_file[0][0]), sep="\n" - ) - wcs = galsim.AstropyWCS(header=header) - - g1_psf_tmp_l = [] - g2_psf_tmp_l = [] - sig_psf_tmp_l = [] - g1_star_tmp_l = [] - g2_star_tmp_l = [] - sig_star_tmp_l = [] - flag_psf_tmp_l = [] - flag_star_tmp_l = [] - - for obj in psf_file[m_ccd_id]: - try: - jac = wcs.jacobian( - world_pos=galsim.CelestialCoord( - ra=obj["RA_LIST"] * galsim.degrees, - dec=obj["DEC_LIST"] * galsim.degrees, - ) - ) - except Exception: - flag_star_tmp_l.append(16) - flag_psf_tmp_l.append(16) - g1_psf_tmp_l.append(0) - g2_psf_tmp_l.append(0) - sig_psf_tmp_l.append(0) - g1_star_tmp_l.append(0) - g2_star_tmp_l.append(0) - sig_star_tmp_l.append(0) - continue - g1_psf_tmp, g2_psf_tmp, sig_psf_tmp = transform_shape( - obj["PSF_MOM_LIST"], jac - ) - - g1_psf_tmp_l.append(g1_psf_tmp) - g2_psf_tmp_l.append(g2_psf_tmp) - sig_psf_tmp_l.append(sig_psf_tmp) - flag_psf_tmp_l.append(obj["PSF_MOM_LIST"][3]) - - g1_star_tmp, g2_star_tmp, sig_star_tmp = ( - transform_shape(obj["STAR_MOM_LIST"], jac) - ) - g1_star_tmp_l.append(g1_star_tmp) - g2_star_tmp_l.append(g2_star_tmp) - sig_star_tmp_l.append(sig_star_tmp) - flag_star_tmp_l.append(obj["STAR_MOM_LIST"][3]) - - new_e1_psf[m_ccd_id] = g1_psf_tmp_l - new_e2_psf[m_ccd_id] = g2_psf_tmp_l - new_sig_psf[m_ccd_id] = sig_psf_tmp_l - new_flag_psf[m_ccd_id] = flag_psf_tmp_l - new_e1_star[m_ccd_id] = g1_star_tmp_l - new_e2_star[m_ccd_id] = g2_star_tmp_l - new_sig_star[m_ccd_id] = sig_star_tmp_l - new_flag_star[m_ccd_id] = flag_star_tmp_l - - exp_cat = np.array( - list( - map( - tuple, - np.array( - [ - new_x, - new_y, - psf_file["RA_LIST"], - psf_file["DEC_LIST"], - new_e1_psf, - new_e2_psf, - new_sig_psf, - psf_file["PSF_MOM_LIST"][:, 3], - new_e1_star, - new_e2_star, - new_sig_star, - psf_file["STAR_MOM_LIST"][:, 3], - new_ccd_id, - psf_file["GLOB_POSITION_IMG_LIST"][:, 0], - psf_file["GLOB_POSITION_IMG_LIST"][:, 1], - ] - ).T.tolist(), - ) - ), - dtype=self._dt_mccd, - ) - cat_list.append(exp_cat) + ), + dtype=self._dt, + ) + cat_list.append(exp_cat) del psf_file @@ -852,10 +262,10 @@ def transform_exposures(self, output_dir, patch, idx, exp_run_dir): return # Finalize catalogue - patch_cat = np.concatenate(cat_list) + star_cat = np.concatenate(cat_list) hdul = fits.HDUList() hdul.append(fits.PrimaryHDU()) - hdul.append(fits.BinTableHDU(patch_cat)) + hdul.append(fits.BinTableHDU(star_cat)) # Write catalogue hdul.writeto( @@ -874,7 +284,6 @@ def run_convert(*args): obj = Convert() obj.set_params_from_command_line(args) - obj.update_params() obj.run() diff --git a/tests/module/test_collate_star_cat.py b/tests/module/test_collate_star_cat.py deleted file mode 100644 index 20d52ce23..000000000 --- a/tests/module/test_collate_star_cat.py +++ /dev/null @@ -1,70 +0,0 @@ -"""UNIT TESTS FOR STAR-CATALOGUE COLLATION PATHS. - -Pin the patch vs patch-less (v2.0) path and filename convention of -``scripts/python/collate_star_cat.py``. Runs up to v1.6 carry a ``P`` -token in both the input run directory and the output filename; v2.0 is -patch-less (``patch is None``) and drops that token, reading from a single -``/output`` root and writing ``validation_psf_conv-.fits`` — the -name still matched by the downstream ``validation_psf_conv-*`` glob. -""" - -import importlib.util -from pathlib import Path - -import pytest - -# The collation script lives under scripts/python (not an importable package), -# so load it by path. -_SCRIPT = ( - Path(__file__).resolve().parents[2] - / "scripts" - / "python" - / "collate_star_cat.py" -) -_spec = importlib.util.spec_from_file_location("collate_star_cat", _SCRIPT) -collate_star_cat = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(collate_star_cat) - - -@pytest.mark.parametrize( - "patch, exp_input, exp_output", - [ - ("3", "in/P3/output/", "out/P3"), - (None, "in/output/", "out"), - ], -) -def test_collate_paths(patch, exp_input, exp_output): - """v1.x carries the P token; v2.0 (patch None) drops it.""" - assert collate_star_cat.collate_paths("in", "out", patch) == ( - exp_input, - exp_output, - ) - - -@pytest.mark.parametrize( - "patch, expected", - [ - ("3", "validation_psf_conv-3-0.fits"), - (None, "validation_psf_conv-0.fits"), - ], -) -def test_output_filename(patch, expected): - """The patch token is present for v1.x and absent for v2.0.""" - assert collate_star_cat.output_filename("validation_psf", patch, 0) == expected - - -def test_output_filename_matches_downstream_glob(): - """Both layouts stay under the downstream ``validation_psf_conv-*`` glob.""" - for patch in ("1", None): - assert collate_star_cat.output_filename( - "validation_psf", patch, 5 - ).startswith("validation_psf_conv-") - - -@pytest.mark.parametrize("bad", ["v2", "2.0", "v1.7", ""]) -def test_invalid_version_raises(bad): - """A mistyped -V is rejected rather than falling through to v1.x.""" - obj = collate_star_cat.Convert() - obj._params["version_cat"] = bad - with pytest.raises(ValueError): - obj.run() From 8f58f59f628a3df9ad5b83ee486c99bd073d3d2b Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:48:22 +0200 Subject: [PATCH 29/36] docs: give the Snakemake workflow a page in the docs `grep -rni snakemake docs/source` returned zero hits: the one production orchestration was documented only in workflow/README.md, which nothing in the Sphinx tree linked. This adds docs/source/workflow.md under "Running on a cluster" -- quick start, the two static invocations and why the exposure set cannot live in the tile DAG, manifests as the DAG's success-only currency, `sp report`, `sp container`, the two roots and the durable products, and the coverage chain -- and points at workflow/README.md for the deep reference. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- docs/source/toc.rst | 1 + docs/source/workflow.md | 189 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 docs/source/workflow.md diff --git a/docs/source/toc.rst b/docs/source/toc.rst index 4faa9f3b9..c94020523 100644 --- a/docs/source/toc.rst +++ b/docs/source/toc.rst @@ -33,6 +33,7 @@ :caption: Running on a cluster clusters + workflow .. toctree:: :hidden: diff --git a/docs/source/workflow.md b/docs/source/workflow.md new file mode 100644 index 000000000..6c3088772 --- /dev/null +++ b/docs/source/workflow.md @@ -0,0 +1,189 @@ +# The Snakemake workflow + +Production ShapePipe runs — a tile list, not a tile — are orchestrated by the +Snakemake workflow in `workflow/`. It is the current and only production +orchestration: the bit-coded bash job scripts and the CANFAR submission layers +that preceded it were retired at `2ef07e45`. + +**Module code is untouched by it.** Every rule ultimately calls +`shapepipe_run -c ` on the config chains described in +[Configuration](configuration.md); the workflow supplies the scheduling, the +per-unit isolation, the bookkeeping and the reclamation around them. Reading +[Basic execution](basic_execution.md) first is still the right order. + +This page is the orientation. The deep reference — design rationale, every +rule, the profile, the two-roots argument — is +[`workflow/README.md`](https://github.com/CosmoStat/shapepipe/blob/develop/workflow/README.md) +in the repository, and the living design record is +[issue #848](https://github.com/CosmoStat/shapepipe/issues/848). + +## Quick start + +```bash +# One-time, on a SHARED filesystem: the SLURM executor re-invokes this python +# inside every job, so a node-local path (/tmp) will not do. +uv venv /project///snakemake-env --python 3.12 +source /project///snakemake-env/bin/activate +uv pip install 'snakemake>=9,<10' 'snakemake-executor-plugin-slurm>=2.7,<3' + +# Edit workflow/config.yaml: tile_list, run_dir, products_dir, container, +# star_cats. + +workflow/bin/sp run # bring products on disk up to date with the tile list +workflow/bin/sp report # the success/failure tables, any time, mid-run is fine +workflow/bin/sp container status # which image the jobs will run inside +workflow/bin/sp cancel # scancel this workflow's jobs +``` + +`workflow/bin/sp` is the entry point for everything. It loads the apptainer +module and the venv, snapshots the code it is about to launch, and sets the +state directory, the SLURM profile and `SP_PHASE`. **Bare `snakemake` outside +`sp` is unsupported** — it skips all of that, including the phase variable the +Snakefile needs to build its index at parse time. Any verb `sp` does not +recognise passes straight through to snakemake with the right profile and state +directory, which is the escape hatch for `sp --unlock`, `sp --dag` or +`sp exp_psf ...`. + +## The two static invocations + +The exposure job set is *derived from data the run itself produces* — a tile's +`find_exposures` output — so it cannot live in the same static DAG that produces +it. `sp run` is therefore two snakemake invocations over one Snakefile: + +1. **PREPARE** (`snakemake prepare_all_tiles`) — the per-tile static chain + `tile_get_images → tile_uncompress → tile_find_exposures`, with `keep-going` + so tile failures stay independent. A nonzero exit is a warning, not a fatal + error: tiles that lost their exposure list are dropped at the compute parse, + and `SP_MISSING_THRESHOLD` is the real gate on how many may be lost. +2. **COMPUTE** (`snakemake all`) — this invocation's *parse* builds the + tile↔exposure index (`run_index.sqlite`) and the DAG then runs the full + exposure and tile chains: + + | level | chain | + | --- | --- | + | exposure | `exp_get_images → exp_star_cat → exp_split → exp_mask → exp_psf → exp_persist → exp_footprint → clean_exposure` | + | tile | `tile_exp_forest → tile_merge_headers → tile_detect → tile_vignets → tile_ngmix → tile_merge_cats → tile_make_cat → clean_tile` | + | campaign | `star_catalogue`, `coverage_map` | + +`sp run` chains both and fails if either phase did. The index **accumulates** +across invocations, so appending tiles to `tile_list` later changes which jobs +exist without invalidating completed work. + +Every rule instance is its own SLURM job (`executor: slurm` in +`profiles/nibi/config.yaml`) carrying that rule's own attempt-scaled resources, +and runs inside the container via snakemake's software-deployment method — the +workflow never calls apptainer itself. + +## Manifests are the currency + +The atom is **one rule == one `shapepipe_run` on one unit** (one tile, or one +exposure), in its own sharded store: `/tiles/<2-char prefix>//` or +`/exp///`. + +A rule's single declared output is not its product files but that unit's +**manifest**, `/manifests/.json`. Per-CCD declaration would +mean millions of paths at DR6 scale, and a missing CCD is frequently +legitimate. + +Manifests are *success-only*. After each run, `completeness.py check` writes its +full verdict — per-runner counts against floors, scraped failure reasons, the +`shapepipe_run` exit status — to the rule's `log:` +(`/logs/.json`) **every** time, and additionally to the +declared manifest **only** when that verdict is a success. So a manifest on disk +means "this stage succeeded", and a resume after an unclean death can never +schedule downstream work on top of a failure. + +Completeness is a **count floor, not a taxonomy**: a stage is a real failure iff +a mandatory runner produced fewer products than its floor. Per-CCD attrition +between the floor and the expected count is tolerated and recorded. There is no +error-signature whitelist. + +## `sp report` + +`sp report` reads the index and those two directories — the manifest for +success, the log for failure, a unit with neither ran nothing — and prints the +per-stage tables plus a `run_report.json` beside the index. It is deliberately +**not** a DAG node: a report rule declaring every tile's output would be a +descendant of every job, so one hard failure under `--keep-going` would poison +its cone and the report would never run in exactly the situation it exists for. +It is a plain script, runnable at any time including mid-run, and the COMPUTE +invocation's `onsuccess`/`onerror` hooks fire it automatically so every run ends +with one. + +## `sp container` + +`sp container` owns which image the jobs run inside, in one resolution order +shared by the CLI and the workflow: **sandbox → your cached SIF → the +`container:` path in `config.yaml`**. + +```bash +sp container status # layers present, active one, revision vs HEAD +sp container pull # ghcr.io/cosmostat/shapepipe:develop-runtime +sp container sandbox # unpack the SIF writable (opt-in) +sp container exec --writable pip install +sp container resolve # just the path the workflow will run +``` + +The cached SIF is a private pull, so nobody else's refresh moves the ground +under your running jobs; the sandbox is the escape hatch for work needing a +package the image does not carry yet. `status` places the image's +`org.opencontainers.image.revision` label against this checkout's HEAD. +`pull` needs the network — run it on a login node, never from a batch job. + +See [Container workflow](container.md) for how the images are built. + +## Durable products + +Two roots. `run_dir` is scratch: the bulk per-unit stores, sized to finish +inside the purge window. `products_dir` is persistent and holds the low-volume +things worth keeping: + +- **Final catalogues** — `/tiles///final_cat-.fits`, + mirroring the scratch shard structure. This file is also the *tile-finished + marker* that lets a completed tile stop pinning its exposures, which is the + second reason it cannot live on scratch. +- **PSF products** — `exp_persist` packs the files named by `persist_exp:` in + `config.yaml` (by default the psfex_interp `validation_psf-*.fits`, the rho/tau + statistics input) into one uncompressed tar per exposure, plus a manifest + recording the members. It answers the scratch purge, not the workflow's own + reclamation, so it runs whether or not `clean:` is on. +- **The index and the report** — `run_index.sqlite`, `missing.json`, + `run_report.json`. +- **Coverage** — see below. + +Exposure stores are reclaimed by the in-DAG `clean_exposure` rule rather than by +`temp()`: exposures overlap tiles, so `temp()` would cascade destructive reruns +whenever a tile is appended. Reclamation leaves a `cleaned.json` tombstone +holding the absorbed manifests, so a reclaimed exposure still reports its +per-runner counts and blocks no tile. + +## Coverage + +Sky coverage is a workflow product, built from records the DAG already writes +rather than by scraping a finished campaign. + +`exp_footprint` writes one JSON per exposure to +`/exp///manifests/exp_footprint.json` giving the +four sky corners of every CCD that got a PSF model. The valid-PSF CCD set comes +off `exp_persist.json`'s tar members — exact, because `psfex_interp` returns +*without* writing `validation_psf-*.fits` on NOT_ENOUGH_STARS, BAD_CHI2 or +FILE_NOT_FOUND — and the WCS off the `headers-.npy` written by `exp_split`. +That makes `validation_psf-*` in `persist_exp:` a precondition of the chain, not +a preference. + +Set `coverage: {enabled: true}` and one further job, `coverage_map`, stamps every +footprint into `/coverage/coverage.hsp` — a HealSparse map counting, +per sky pixel, the exposures with a valid PSF there. It is **campaign-cumulative**: +its declared inputs are the in-scope footprints, but the script reads every record +on the products root, reclaimed exposures included, so appending tiles grows the map +instead of replacing it. `nside` is set in `config.yaml` to the production +128/131072 pair, chosen to align pixel-wise with the UNIONS bit masks — nothing +defaults to it, and a coarser map would look plausible and silently fail to align. + +Plotting stays out of the DAG, as a human act on a durable product: + +```bash +plot_coverage_map -i /coverage/coverage.hsp ... +``` + +with the sky windows under `coverage.plot` in `config.yaml`. From bd8bf434a60160481e2a3a807cfc2308d8896bcd Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:51:47 +0200 Subject: [PATCH 30/36] docs: rebuild the tutorial on the rule chain Every runnable command on the introductory tutorial was dead: nine invocations of `job_sp -j `, and `scripts/sh/job_sp.bash` has not existed for some time. The bit-coded spine is replaced by the workflow's rules, stage by stage, each section now naming the rule that does the work and the config it runs. Kept unchanged: the file-type, naming and numbering conventions (the most durable content in docs/source) and all the diagnostic commentary -- the ds9 flag comparison, the star-selection plots, the star_stat example, the stats_global histograms. Also corrected while passing through: masking now says there is no tile_mask rule (the committed chain is the unmasked tile_detect variant) and that the star catalogues come from the exp_star_cat cache rather than being staged by hand; the MCCD alternatives are dropped from the PSF and galaxy sections, since no workflow config names an mccd runner. The "Upload results" section goes with the `-j 128` step it documented. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- docs/source/pipeline_tutorial.md | 229 +++++++++++++++---------------- 1 file changed, 114 insertions(+), 115 deletions(-) diff --git a/docs/source/pipeline_tutorial.md b/docs/source/pipeline_tutorial.md index 95a0aa94a..73b9d1afd 100644 --- a/docs/source/pipeline_tutorial.md +++ b/docs/source/pipeline_tutorial.md @@ -2,13 +2,17 @@ ## Quick start -Run the entire pipeline on a single example CFIS image with tile ID 246.290: -1. [Install](installation.md) `ShapePipe` — the recommended path is the container image, which bundles everything needed to run the pipeline. -3. Run the job script +Production runs go through the [Snakemake workflow](workflow.md). Put the tile +IDs in the file named by `tile_list` in `workflow/config.yaml`, one +`IDra.IDdec` per line, and run + ```bash -job_sp 246.290 -j 127 +workflow/bin/sp run ``` +A single tile is a legal tile list and is the smallest useful end-to-end run. +The sections below describe what that produces, stage by stage. + ## Introduction The `ShapePipe` pipeline processes single-exposure images and stacked images. Input images have to be calibrated beforehand for astrometry and photometry. This tutorial of an entire `ShapePipe` run covers specifically images from CFIS, the Canada-France Imaging Survey. CFIS stacks are so-called tiles, which are the co-adds of on average three exposures in the r-band. @@ -94,7 +98,7 @@ Naming and numbering of the input files can closely follow the original image na `ShapePipe` splits the processing of CFIS images into several parts: These are the retrieval and preparation of input images, processing of single exposures, -processing of tile images, creation and upload (optional) of _final_ shape catalogues. +processing of tile images, and creation of _final_ shape catalogues. The following flowchart visualised the processing parts and steps. @@ -104,46 +108,58 @@ Below, the individual processing steps are described in detail. ### Input and output paths -All required paths are automatically set in the job script `job_sp`. +The workflow's rules export every path a config interpolates before each +`shapepipe_run` call, so nothing here needs setting by hand for a normal run. -If an example config file is run outside this script, -the following path variables might need to be defined. +If a config file is run outside the workflow, the following variables need to be +defined. - `$SP_RUN`: Run directory of `ShapePipe`. In general this is just `pwd`, and can be set via ```bash export SP_RUN=`pwd` ``` but on a cluster this directory might be different. - `$SP_CONFIG`: Path to configuration files. In our example this is `$SP_BASE/example/cfis`. +- `$SP_UNIT_NUM`: the tile or exposure this run is restricted to, in ShapePipe's + image-number convention — a leading dash, and dots replaced by dashes (tile + `210.282` becomes `-210-282`, exposure `2605805` becomes `-2605805`). The + configs interpolate it into `NUMBER_LIST`. In addition, the output path `$SP_RUN/output` needs to be created by the user before running `ShapePipe`. -### Job and pipeline scripts +### The rule chain -The job script to run the pipeline in its entity or in parts is `job_sp[.bash]`. Type -```bash -job_sp -h -``` -for all options. +There is no monolithic job script. Every processing step below is a **rule**, and +every rule instance is one `shapepipe_run` call on one unit — one tile, or one +single exposure — inside that unit's own directory: -This script creates the subdirectory `$SP_RUN/output` to store all pipeline outputs -(log files, diagnostics, statistics, output images, catalogues, single-exposure headers with WCS information). +| level | rule chain | +| --- | --- | +| tile, prepare phase | `tile_get_images` → `tile_uncompress` → `tile_find_exposures` | +| exposure | `exp_get_images` → `exp_star_cat` → `exp_split` → `exp_mask` → `exp_psf` → `exp_persist` → `exp_footprint` → `clean_exposure` | +| tile, compute phase | `tile_exp_forest` → `tile_merge_headers` → `tile_detect` → `tile_vignets` → `tile_ngmix` → `tile_merge_cats` → `tile_make_cat` → `clean_tile` | -Optionally, the subdir `output_star_cat` is created by the used to store the external star catalogues for masking. This is only necessary if the pipeline is run on a cluster without internet connection to access star catalogues. In that case, the star catalogues need to be retrieved outside the pipeline, for example on a login node, and copied to `output_star_cat`. +Each rule calls -The job script automaticall performs a number of subsequent calls to the `ShapePipe` executable `shapepipe_run`, as ```bash shapepipe_run -c $SP_CONFIG/.ini ``` -The config file `.ini` contains the configuration for one or more modules. -See the main `ShapePipe` readme for more details. -The user specifies which steps are run with the command line option `-j JOB`. The integer value `JOB` -is bit-coded such that arbitrary combinations of steps can be run with a single call to `job_sp`. For -example, to run steps #1 and #2, type `job_sp -j 3`. +on a config file committed under `workflow/config/cfis/`, containing the +configuration for one or more modules. See [Configuration](configuration.md) for +what goes in one. To run a single rule and its prerequisites and nothing else, +name it as a snakemake target: `sp exp_psf ...`. + +Each unit directory holds an `output/` subdirectory with all pipeline outputs +(log files, diagnostics, statistics, output images, catalogues, single-exposure +headers with WCS information), beside the `manifests/` and `logs/` that are the +workflow's own bookkeeping. ### Select tiles -To run the job script, one or more CFIS tiles need to be chosen. If the tile IDs are known, they are provided to `job_sp` on the command line. +The tiles to process are the lines of the tile list. The campaign grows by +appending to that file: the tile-to-exposure index accumulates across +invocations, so adding tiles later changes which jobs exist without invalidating +completed work. If the tile IDs are not known a priori, they can be selected via sky coordinates, with the script `cfis_field_select`. For example, to find the tile number for a Planck cluster at R.A.=213.68 deg, dec=57.79 deg, run: @@ -153,54 +169,57 @@ cfis_field_select -i /path/to/shapepipe/auxdir/CFIS/tiles_202007/tiles_all_order The input text file (provide via the flag `-i`) contains a list of CFIS tiles, this can also be directory containing the tile FITS files. -The following sections describe the different steps that are performed with `job_sp`. +The following sections describe the steps the rule chain performs. ## Run the pipeline ### Retrieve input images -The command -```bash -job_sp TILE_ID -j 1 -``` -retrieves the image and weight corresponding to TILE_ID using the module `get_images`. -It then identifies the exposures that were used to create the tile image via the `find_exposures` runner. -Finally, another call to `get_images` retrieves the exposure images, weights, and flag files. +`tile_get_images` retrieves the image and weight corresponding to a tile ID +using the module `get_images`. `tile_find_exposures` then identifies the +exposures that were used to create the tile image via the `find_exposures` +runner, and `exp_get_images` retrieves each of those exposures' image, weight +and flag files. -For the retrieval method the user can choose betwen -- download from VOspace (`-r vos`); -- create symbolic link to existing file on disk (`-r symlink`). +For the retrieval method the user can choose between +- download from VOspace (`RETRIEVE = vos` in the config); +- create symbolic link to existing file on disk (`RETRIEVE = symlink`). Note that internet access is required for this step if the download method is `vos`. -An output directory `run_sp_GitFeGie` (in `output`) is created containing the results of `get_images` for tiles (`Git`), -`find_exposures` (`Fe`), and `get_images` for exposures (`Gie`). +These three rules are the workflow's PREPARE phase. The exposure half of the run +cannot be scheduled before them, because which exposures exist is something +`find_exposures` has to say first — which is why one run is two snakemake +invocations over one Snakefile (see [the workflow page](workflow.md)). ## Prepare input images -With -```bash -job_sp TILE_ID -j 2 -``` -the compressed tile weight image is uncompressed via the `uncompress_fits` module. Then, the single-exposure images, weight, and flags are split into single-exposure single-CCD file -(one FITS file per CCD) with `split_exp`. -Finally, the headers of all single-exposure single-CCD files are merged into a single `sqlite` file, to store the WCS information of the input exposures. +`tile_uncompress` uncompresses the compressed tile weight image via the +`uncompress_fits` module. + +`exp_split` splits each single-exposure image, weight and flag into +single-exposure single-CCD files (one FITS file per CCD) with `split_exp`, and +writes that exposure's WCS information to `headers-.npy`. -Two output directories are created, `run_sp_Uz` for `uncompress_fits`, and `run_sp_exp_SpMh` for the output of the modules -`split_exp` (`Sp`) and `merge_headers` (`Mh`). +`tile_merge_headers` then merges the headers of the exposures overlapping one +tile into a single `sqlite` file, so the tile-side modules can look up the WCS of +any of them. ## Mask images -Run -```bash -job_sp TILE_ID -j 4 -``` -to mask tile and single-exposure single-CCD images. Both tasks are performed by two calls to the `mask` runner. +`exp_mask` masks the single-exposure single-CCD images with the `mask` runner. -Note that internet access is required for this step, since a reference star catalogue is downloaded. +Masking needs a reference star catalogue, and no compute node needs internet +access to get one: `star_catalogue` makes one Vizier cone query per HEALPix +chunk of the campaign footprint into a run-independent cache, and `exp_star_cat` +cuts each exposure's catalogue out of that cache with no network at all. Both +are `localrule`s, so the queries run in the head process. Network cost therefore +scales with sky area rather than with exposure count. -The output of both masking runs are stored in the output directory `run_sp_MaMa`, with run 1 (2) of -`mask` corresponding to tiles (exposures). +There is no `tile_mask` rule: the committed config chain is the unmasked +`tile_detect` variant. Adding the masked variant is a config plus one rule, and +needs a tile-side analogue of `exp_star_cat` — tile star catalogues key on tile +ID, so they are a separate cache namespace. **Diagnostics:** Open a single-exposure single-CCD image and the corresponding pipeline flag in `ds9`, and display both frames next to each other. Example @@ -218,23 +237,23 @@ have a zero-padded pixel border, which is not accounted for by `ds9`. ## Detect objects on tiles and process stars on single exposures -The call -```bash -job_sp TILE_ID -j 8 -``` -performs a number of steps. First, objects on the tiles are deteced with the `sextractor` runner. -Next, the following tasks are run on the single-exposure single-CCD images: -- Objects are deteced with `sextractor`. +`tile_detect` detects objects on the tile with the `sextractor` runner. + +`exp_psf` runs the single-exposure single-CCD chain: +- Objects are detected with `sextractor`. - Star candidates are selected via `setools`. -- The PSF model is created, either with `psfex` for PSFex, or - with `mccd_preprocessing` and `mccd_fit_val` for MCCD. -- The PSF model is interpolated to star positions for validation. For the PSFEx model, this is done - via a call to `psfex_interp`. For MCCD, the modules `merge_starcat`, `mccd_plots`, and - `mccd_interp` are called. +- The PSF model is created with `psfex`. +- The PSF model is interpolated to star positions for validation, via a call to + `psfex_interp`. + +The output directory is `run_sp_exp_SxSePsfPi`, holding the output of SExtractor +on the exposures (`Sx`), `setools` (`Se`), the PSF model (`Psf`) and the +validation interpolation (`Pi`). -The output directory for both the `mccd` and `psfex` options is `run_sp_tile_Sx_exp_SxSePsf`. -This stores the output of SExtractor on the tiles (`tile_Sx`), on the exposures (`exp_Sx`), -`setools` (`Se`), and the Psf model (`Psf`). +`exp_persist` then copies the products named by `persist_exp:` in +`workflow/config.yaml` — by default the `validation_psf-*.fits` that the rho and +tau statistics are computed from — off scratch onto the persistent root, before +the purge or `clean_exposure` can take them. The following plots show an example of a single CCD, in the center of the focal plane. @@ -282,60 +301,40 @@ several runs. ## Galaxy selection -The focus of the next step, -```bash -job_sp TILE_ID -j 16 -``` -is the selection of galaxies as extended objects compared to the PSF. -First, the PSF model is interpolated to galaxy positions, according to the PSF model -with `psfex_interp` or `mccd_interp`. Next, postage stamps around galaxies -of the weights maps are created via `vignetmaker`. Then, the spread model -is computed by the `spread_model` module. Finally, postage stamps -around galaxies of single-exposure data is extracted with another call -to `vignetmaker`. - -The output directory is -- `run_sp_MiViSmVi` if the PSF model is `mccd`; -- `run_sp_tile_PsViSmVi` for the `PSFEx` PSF model. +`tile_vignets` selects galaxies as extended objects compared to the PSF. +First, the PSF model is interpolated to galaxy positions with `psfex_interp`. +Next, postage stamps around galaxies of the weight maps are created via +`vignetmaker`. Then the spread model is computed by the `spread_model` module. +Finally, postage stamps around galaxies of single-exposure data are extracted +with another call to `vignetmaker` — `Pi`, `Vi`, `Sm`, `Vi`, hence the config +name `config_tile_PiViVi.ini`. -This corresponds to the MCCD/PSFex interpolation (`Mi`/`Pi`), `vignetmaker` (`Vi`), `spread_model` (`Sm`), and the -second call to `vignetmaker` (`Vi`). +`tile_vignets` reads the exposure products through the symlink forest that +`tile_exp_forest` built for this tile, and is the last stage that touches them: +once every consuming tile has its vignets, an exposure store becomes +reclaimable. ## Shape measurement -The call -```bash -job_sp TILE_ID -j 32 -``` -computes galaxy shapes using the multi-epoch model-fitting method `ngmix`. At the same time, -shapes of artifically sheared galaxies are obtained for metacalibration. +`tile_ngmix` computes galaxy shapes using the multi-epoch model-fitting method +`ngmix`. At the same time, shapes of artificially sheared galaxies are obtained +for metacalibration. -Shape measurement is performed in parallel for each tile, the number of processes can be specified -by the user with the option `--nsh_jobs NJOB`. This creates `NJOB` output directories `run_sp_tile_ngmix_Ngu`. -with `X` = 1 ... `NJOB` containing the result of `ngmix`. +Shape measurement is split into parallel chunks: `ngmix_chunks:` in +`workflow/config.yaml` sets how many, and each chunk is its own rule instance +writing its own `run_sp_tile_ngmix_Ngu` output directory. ## Paste catalogues -The last real processing step is -```bash -job_sp TILE_ID -j 64 -``` -This task first merges the `NJOB` parallel `ngmix` output files from the previous step into -one output file. Then, previously obtained information are pasted into a _final_ shape catalogue via `make_cat`. -Included are galaxy detection and basic measurement parameters, the PSF model at -galaxy positions, the spread-model classification, and the shape measurement. - -Two output directories are created. -The first one is `run_sp_Ms` for the `merge_sep` run. -The second is `run_sp_Mc` for the `make_cat` task; the name is the same for both the `MCCD` and `PSFEx` PSF model. - - -## Upload results - -Optionally, after the pipeline is finished, results can be uploaded to VOspace via -```bash -job_sp TILE_ID -j 128 -``` +`tile_merge_cats` merges the parallel `ngmix` output files from the previous +step into one file with `merge_sep`, in `run_sp_Ms`. +`tile_make_cat` then pastes the previously obtained information into a _final_ +shape catalogue via `make_cat`, in `run_sp_Mc`. Included are galaxy detection +and basic measurement parameters, the PSF model at galaxy positions, the +spread-model classification, and the shape measurement. The rule publishes that +catalogue onto the persistent root as +`/tiles///final_cat-.fits` — the campaign's +product, and also the marker that says this tile is finished. From b58551d9b1db3735f7012b1bc5fe432ee0de9f00 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:52:32 +0200 Subject: [PATCH 31/36] docs(workflow): correct the README's drift and name the decision Three things had drifted apart from the code: - The opening sentence spoke of the bash layers in the present tense, as something this workflow "replaces". They are retired; say so, and say plainly that this is the one production orchestration. - `sp_rule.py` does not exist and never did on this branch. The README named it in the Layout block, in completeness.py's description and in the count-floor bullet, and config_exp_Sp.ini cited it for the dashed-ID convention. The real mechanism is the Snakefile's `unit_pre()` (the furniture and the run-dir clear, inlined as params bash) and `sp_shell()` (prologue, one shapepipe_run, one `completeness.py check`); the dashed ID comes from `unit_num()`. - The Layout block omitted three scripts that are there: star_cats.py, ngmix_range.py, clean_tile.py. Adds the one line the retirement turns on: no catalogue version in the code, no sky patches, git tags are the version. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- workflow/README.md | 34 +++++++++++++++++--------- workflow/config/cfis/config_exp_Sp.ini | 4 +-- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/workflow/README.md b/workflow/README.md index 0d3a35cba..8ece7682a 100644 --- a/workflow/README.md +++ b/workflow/README.md @@ -1,14 +1,21 @@ # ShapePipe Snakemake orchestration -Snakemake workflow that orchestrates real-data ShapePipe runs. It replaces the -`run_job_sp_canfar_v2.0.bash → job_sp_canfar_v2.0.bash` bash layers, the CANFAR -submission front end that drove them (`curl_canfar_local.sh`, -`canfar_submit_job`, retired at `2ef07e45`), and the per-site sbatch -reimplementations. **Module code is untouched**: rules call +Snakemake workflow that orchestrates real-data ShapePipe runs. It is the one +production orchestration. It replaced the bit-coded bash layers +(`run_job_sp_canfar_v2.0.bash → job_sp_canfar_v2.0.bash`), the CANFAR submission +front end that drove them (`curl_canfar_local.sh`, `canfar_submit_job`) and the +per-site sbatch reimplementations; all of those were retired, last carried at +`2ef07e45`. **Module code is untouched**: rules call `shapepipe_run -c ` on the existing config chains. Design and rationale: [CosmoStat/shapepipe#848](https://github.com/CosmoStat/shapepipe/issues/848) (the living PRD). +**There is no concept of a catalogue version in the code.** No path branches on +`v1.3`..`v1.6` or `v2.0`, and there are no sky patches: a campaign is a tile +list, and the version of a catalogue is the git tag of the code that produced +it. What that retirement removed, and where each piece was last carried, is in +the PR that made this sentence true. + Use `workflow/bin/sp` for everything. Bare `snakemake all` outside `sp` is unsupported: `sp` sets the state directory, the SLURM profile, and `SP_PHASE`, which the Snakefile needs to build the tile/exposure index at @@ -159,16 +166,18 @@ workflow/ tile.smk per-tile: exp forest, merge_headers, mask, detect, vignets, ngmix, merge, make_cat coverage.smk campaign-level: the HealSparse nexp mask from the exposure footprints scripts/ - sp_rule.py the thin per-unit wrapper (isolation furniture, config copy, log-sync, count floor) build_index.py prepare-phase run_index.sqlite builder (plain script) build_forest.py per-tile exposure symlink forest (group-compatible shell) - completeness.py the ported count-floor table (shared by sp_rule + run_report) + completeness.py the ported count-floor table + the `check` every rule ends with run_report.py standalone report (NOT a DAG node; run_report hooks call it) container.py image layers + the resolution order behind `sp container` (stdlib-only) + star_cats.py the campaign's GSC 2.3 sky store + the per-exposure cuts from it + ngmix_range.py the ngmix chunk partition: written once by tile_vignets, read by each chunk persist_exp.py ONE exposure's keepable PSF products -> one tar on products_dir (the exp_persist rule) exp_footprint.py ONE exposure's per-CCD sky corners, for the CCDs with a PSF (the exp_footprint rule) coverage_map.py every exposure footprint on products_dir -> coverage.hsp (the coverage_map rule) clean_exposure.py ONE exposure's store + manifests + logs -> tombstone (the clean_exposure rule) + clean_tile.py ONE finished tile's store -> tombstone (the clean_tile rule) profiles/nibi/config.yaml SLURM executor; apptainer SDM; per-user jobs cap; keep-going ``` @@ -190,10 +199,13 @@ profiles/nibi/config.yaml SLURM executor; apptainer SDM; per-user jobs cap; kee output natively and never touches its log, which is why the profile runs *without* `keep-incomplete`. `sp report` reads both dirs — the manifest for success, the log for failure — and a unit with neither ran nothing. -- **Completeness is a count floor, not a taxonomy.** After a run, - `sp_rule.py` counts products per mandatory runner against - `completeness.py`'s floor and exits nonzero below it. Per-CCD attrition - between floor and `expect` is tolerated. No 3-class taxonomy, no +- **Completeness is a count floor, not a taxonomy.** There is no per-unit + wrapper script: the Snakefile's `unit_pre()` builds the unit's furniture and + clears the stage's run dir as bash inlined into the rule's `params`, and + `sp_shell()` composes every rule's shell as *prologue, one `shapepipe_run`, + one `completeness.py check`*. That check counts products per mandatory runner + against `completeness.py`'s floor and exits nonzero below it. Per-CCD + attrition between floor and `expect` is tolerated. No 3-class taxonomy, no error-signature whitelist. `--keep-going` isolates a failure to its own DAG cone. - **Stores are sharded.** Every tile/exposure runs its own `shapepipe_run` diff --git a/workflow/config/cfis/config_exp_Sp.ini b/workflow/config/cfis/config_exp_Sp.ini index dd27d6ccd..3b573cbc4 100644 --- a/workflow/config/cfis/config_exp_Sp.ini +++ b/workflow/config/cfis/config_exp_Sp.ini @@ -34,8 +34,8 @@ LOG_NAME = log_sp # Runner log file name, optional, default: shapepipe_runs RUN_LOG_NAME = log_run_sp -# NUMBER_LIST selects this unit; the workflow sets SP_UNIT_NUM to the -# dashed exposure ID (exp split is a "tile-scheme" stage per sp_rule.py). +# NUMBER_LIST selects this unit; the workflow sets SP_UNIT_NUM to the dashed +# exposure ID (the Snakefile's unit_num(), exported by unit_pre()). NUMBER_LIST = $SP_UNIT_NUM # Input directory, containing input files, single string or list of names with length matching FILE_PATTERN From bde683b35964882ea75d7cb07fbf8ada2d36ca07 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:53:54 +0200 Subject: [PATCH 32/36] docs: point the top-level docs at the workflow, and say what is in scripts/ CLAUDE.md's Layout block had no entry for `workflow/` at all -- the one production orchestration was invisible to anyone reading the repo map. Adds it, naming bin/sp, the rule files, the scripts, the config chain and the profile, and adds the sentence the retirement turns on so the decision does not have to be rediscovered: no catalogue version in the code, no sky patches, git tags are the version. README.rst gains a workflow bullet beside the cluster one. scripts/README.rst described two scripts, one of which (`create_log_exp_headers`) has not existed for years, and gave a usage line for the other that the script's own --help supersedes. Rewritten as a map of what is actually in the directory, grouped by what the scripts are for, with the container's bare-name symlinking stated once. It deliberately does not restate any script's interface -- that is what drifted. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- CLAUDE.md | 14 ++++++++ README.rst | 1 + scripts/README.rst | 90 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 86 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fba149932..b408ed15b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,12 +87,26 @@ Full detail: `docs/source/installation.md` and `docs/source/container.md`. unit/property/integration tests), `unit/` (structural), `science/` (fast guardrails), `cluster/` (candide-only), `helpers/` (shared library code). See `tests/README.md`. +- `workflow/` — **the production orchestration**: a Snakemake workflow over the + same `shapepipe_run` calls. `bin/sp` is the entry point (`run`, `report`, + `container`, `cancel`); `Snakefile` plus `rules/{prepare,exposure,tile, + coverage}.smk` are the rule graph; `scripts/` holds the plain Python each rule + shells out to; `config.yaml` declares one run (tile list, the scratch and + persistent roots, the container); `config/cfis/` holds the committed ini + chain; `profiles/nibi/config.yaml` is the SLURM executor profile. Deep + reference: `workflow/README.md`. User-facing: `docs/source/workflow.md`. - `example/` — a runnable example pipeline (`example/config.ini`) on a single CFIS tile; doubles as the CI smoke test. - `scripts/` — shell / Python / notebook helpers (`sh/`, `python/`, `jupyter/`), symlinked onto `$PATH` inside the image. - `docs/` — Sphinx sources; the API docs are generated from docstrings. +**There is no concept of a catalogue version in the code.** Nothing branches on +`v1.3`..`v1.6` or `v2.0`, and there are no sky patches (`P1`..`P9`): a campaign +is a tile list, and the version of a catalogue is the git tag of the code that +produced it. The pre-Snakemake bash and CANFAR layers that carried those +constructs were retired, last carried at `2ef07e45`. + ## Development workflow - **`develop` is the integration branch** — open PRs against it. `main` / diff --git a/README.rst b/README.rst index 40999f6fd..01303fe4e 100644 --- a/README.rst +++ b/README.rst @@ -51,6 +51,7 @@ to start: - `Basic execution `_ and `configuration `_ — running ``shapepipe_run`` and writing pipeline configs. - `Container workflow `_ — the two image targets and the ``pyproject.toml`` / ``uv.lock`` / ``Dockerfile`` layers. - `Running on a cluster `_ — pulling the image and submitting jobs, with a worked candide (SLURM) example. +- `The Snakemake workflow `_ — the production orchestration for a whole tile list, driven by ``workflow/bin/sp``. If you use ShapePipe in academic work, please cite Guinot et al. (2022) and Farrens et al. (2022). diff --git a/scripts/README.rst b/scripts/README.rst index 1d3e265a6..e1f3d0bec 100644 --- a/scripts/README.rst +++ b/scripts/README.rst @@ -1,28 +1,80 @@ Scripts directory ================= -This directory contain scripts that are used on pipeline outputs or to prepare -files. They are not run throught the pipeline framework. For more details on how -to run each them see below. +Helpers that operate on pipeline inputs and outputs but are **not** pipeline +modules: nothing here is run by ``shapepipe_run``, and nothing here is part of +the Snakemake workflow's rule graph (that is ``workflow/scripts/``). -Python scripts -============== +Everything under ``scripts/*/`` with a ``.py``, ``.sh`` or ``.bash`` extension is +symlinked onto ``$PATH`` inside the container image under its bare name, so +``create_star_cat`` and ``python scripts/python/create_star_cat.py`` are the same +command. -1. `create_log_exp_headers`_ -2. `create_star_cat`_ +Each script's own ``--help`` and module docstring is the reference; this file is +only a map. Run any of them with ``-h`` first. -create_log_exp_headers -====================== +``python/`` — pipeline inputs +----------------------------- -This as to run after the module `split_exp_runner` it will create a master log -file containing all the WCS information for each CCDs of each single exposures. -To run the script : -`python create_log_exp_headers.py path/to/split_exp_runner/output path/to/srcipt/output_dir` +- ``cfis_field_select.py`` — select CFIS tiles or exposures by sky coordinates + or by name, and emit the ID list. This is how a tile list for + ``workflow/config.yaml`` is made. +- ``create_star_cat.py`` — build reference star catalogues for the mask module. + Superseded for exposures by the workflow's ``exp_star_cat`` rule; its + ``-k tile`` mode is the prerequisite for a future tile-mask rule. +- ``check_tile_coverage.py`` — flag input simulation tiles whose weight maps are + mostly empty, as a YAML exclude list. Called from sp_validation's image-sims + orchestration, not from here. -create_star_cat -=============== +``python/`` — pipeline outputs +------------------------------ -This script create all the star catalogs required to run the mask module for a -computational node without internet access. -To run the script : -`python create_star_cat.py path/to/image_dir path/to/script/output_dir` +- ``collate_star_cat.py`` — collate the per-exposure PSF validation catalogues + into the input ``merge_starcat`` wants for the rho/tau statistics. +- ``create_final_cat.py`` — merge per-tile final catalogues into one HDF5 file + for sp_validation. +- ``stats_global.py`` — histograms and tables from the SETools per-CCD summary + statistics of a whole run. + +``python/`` — image simulations +------------------------------- + +The image-simulation chain is still driven by the pre-Snakemake bash layer, +which sp_validation's ``image_sims.smk`` calls into. These exist for it and are +not used by real-data runs. + +- ``init_run_v2.0.py`` — lay out a run directory for that layer. +- ``update_runs_log_file.py`` — rebuild ``log_run_sp.txt`` from the run + directories on disk, which that layer needs because it deletes run dirs. +- ``test_tile_det.py`` + ``test_tile_det.cfg`` — drive tile detection over it. + +``python/`` — the ngmix status dashboard +---------------------------------------- + +Orthogonal to everything above: ``build_status.py``, ``build_history.py``, +``plot_trends.py``, ``run_breakdown_grid.py``, ``plot_breakdown_grid.py`` and +``plot_s4_ablations.py`` build the shape-measurement status page and its +trend/ablation figures. + +``sh/`` +------- + +- ``shapepipe_run_example.sh`` — run the bundled example pipeline against a + writable copy of ``/app/example``. This is the container's CI smoke test. +- ``apptainer_noslurm.sh`` — ``apptainer exec`` with the SLURM/PMI/PMIX/OMPI + environment stripped, for MPI inside a SLURM job. +- ``run_job_sp_canfar_v2.0.bash``, ``job_sp_canfar_v2.0.bash``, + ``job_list_help.bash``, ``functions.sh`` — the bit-coded bash job layer. Kept + only for image simulations (above); real-data runs go through ``workflow/``. + +``jupyter/`` +------------ + +Scratch notebooks: ``wcs.ipynb``, ``test_centroid_shift.py``. + +``validation/`` +--------------- + +``centroid/`` — the metacalibration multiplicative-bias validation of the ngmix +centroid handling: ``run_all.sh`` drives ``centroid_bias_v2.py`` over the three +cases in ``configs/``. From f0825fecc611c282eed47bd8aafd75f6e26dbade Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:54:40 +0200 Subject: [PATCH 33/36] docs: repoint the tutorial's three dead in-page anchors `#select-stars`, `#download-tiles` and `#find-exposures` named headings this page has never had. Point them at the sections that actually describe those steps. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- docs/source/pipeline_tutorial.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/pipeline_tutorial.md b/docs/source/pipeline_tutorial.md index 73b9d1afd..cb701c6b8 100644 --- a/docs/source/pipeline_tutorial.md +++ b/docs/source/pipeline_tutorial.md @@ -73,21 +73,21 @@ Naming and numbering of the input files can closely follow the original image na sheared images. This information is used in post-processing to compute calibrated shear estimates via metacalibration. - Summary statistic files - The `SETools` module that creates samples of objects according to some user-defined selection criteria (see [Select stars](#select-stars)) also outputs ASCII + The `SETools` module that creates samples of objects according to some user-defined selection criteria (see [Detect objects](#detect-objects-on-tiles-and-process-stars-on-single-exposures)) also outputs ASCII files with user-defined summary statistics for each CCD, for example the number of selected stars, or mean and standard deviation of their FWHM. Example: `star_stat-2366993-18.txt` - Tile ID list - ASCII file with a tile number on each line. Used for the `get_image_runner` module to download CFIS images (see [Download tiles](#download-tiles)). + ASCII file with a tile number on each line. Used for the `get_image_runner` module to download CFIS images (see [Retrieve input images](#retrieve-input-images)). - Single-exposure name list ASCII file with a single-exposure name on each line. Produced by the `find_exposure_runner` module to identify single exposures that were used to create - a given tile. See [Find exposures](#find-exposures)). + a given tile. See [Retrieve input images](#retrieve-input-images)). - Plots The `SETools` module can also produce plots of the objects properties that were selected for a given CCD. The type of plot (histogram, scatter plot, ...) and quantities to plot as well as plot decorations can be specified in the - selection criteria config file (see [Select stars](#select-stars)). + selection criteria config file (see [Detect objects](#detect-objects-on-tiles-and-process-stars-on-single-exposures)). Example: `hist_mag_stars-2104133-5.png` - Log files From 4820d4cad0d11771ce65d510a7814677230b7c2f Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 19:57:10 +0200 Subject: [PATCH 34/36] docs: let MyST generate heading anchors The tutorial's in-page links (`[Mask images](#mask-images)` and friends) each raised a `myst.xref_missing` warning on every build, because MyST emits no heading targets unless asked for them. `myst_heading_anchors = 3` covers h1-h3, which is every heading those links point at, and clears five warnings. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- docs/source/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index eea9b8f43..d52836d43 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -104,6 +104,10 @@ myst_enable_extensions = ["html_image"] +# Generate slug anchors for h1-h3 so in-page links like +# `[Mask images](#mask-images)` resolve instead of warning. +myst_heading_anchors = 3 + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for From 1a59a95455bd935b6bccf30f0e254a91c67b2255 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sat, 5 Sep 2026 20:11:35 +0200 Subject: [PATCH 35/36] docs: say plainly that the Gen-2 bash layer is still here MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit workflow/README.md, docs/source/workflow.md and CLAUDE.md each asserted that the bit-coded bash job layer was retired at 2ef07e45 along with the CANFAR front end. The CANFAR half is true; the bash half is not. scripts/sh/ still carries run_job_sp_canfar_v2.0.bash, job_sp_canfar_v2.0.bash, job_list_help.bash and functions.sh, pyproject's [tool.setuptools] script-files still installs the first two, and sp_validation's image-sims workflow calls run_job_sp_canfar_v2.0.bash by path — a live cross-repo dependency a reader of those three pages would have concluded was gone. scripts/README.rst already said this correctly. Each page now names what actually went and why the pair stays. The Gen-2 scripts carry no catalogue-version or patch constructs (verified by grep), so keeping them is consistent with the decision; they retire when the image-sims chain is ported or parked. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- CLAUDE.md | 7 +++++-- docs/source/workflow.md | 8 +++++--- workflow/README.md | 12 ++++++++---- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b408ed15b..cae6d99eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,8 +104,11 @@ Full detail: `docs/source/installation.md` and `docs/source/container.md`. **There is no concept of a catalogue version in the code.** Nothing branches on `v1.3`..`v1.6` or `v2.0`, and there are no sky patches (`P1`..`P9`): a campaign is a tile list, and the version of a catalogue is the git tag of the code that -produced it. The pre-Snakemake bash and CANFAR layers that carried those -constructs were retired, last carried at `2ef07e45`. +produced it. The CANFAR layers and the summary scrape that carried those +constructs were retired, last carried at `2ef07e45`. The pre-Snakemake bash job +layer (`scripts/sh/run_job_sp_canfar_v2.0.bash`, `job_sp_canfar_v2.0.bash`, +`job_list_help.bash`, `functions.sh`) stays — it is version-free, and +sp_validation's image-simulation workflow calls it. ## Development workflow diff --git a/docs/source/workflow.md b/docs/source/workflow.md index 6c3088772..49175beed 100644 --- a/docs/source/workflow.md +++ b/docs/source/workflow.md @@ -1,9 +1,11 @@ # The Snakemake workflow Production ShapePipe runs — a tile list, not a tile — are orchestrated by the -Snakemake workflow in `workflow/`. It is the current and only production -orchestration: the bit-coded bash job scripts and the CANFAR submission layers -that preceded it were retired at `2ef07e45`. +Snakemake workflow in `workflow/`. It is the current and only orchestration for +real data: the CANFAR submission layers that preceded it were retired at +`2ef07e45`, while the bit-coded bash job scripts they drove +(`run_job_sp_canfar_v2.0.bash`, `job_sp_canfar_v2.0.bash`) survive only as the +entry point of sp_validation's image-simulation chain. **Module code is untouched by it.** Every rule ultimately calls `shapepipe_run -c ` on the config chains described in diff --git a/workflow/README.md b/workflow/README.md index 8ece7682a..74d1279c8 100644 --- a/workflow/README.md +++ b/workflow/README.md @@ -2,10 +2,14 @@ Snakemake workflow that orchestrates real-data ShapePipe runs. It is the one production orchestration. It replaced the bit-coded bash layers -(`run_job_sp_canfar_v2.0.bash → job_sp_canfar_v2.0.bash`), the CANFAR submission -front end that drove them (`curl_canfar_local.sh`, `canfar_submit_job`) and the -per-site sbatch reimplementations; all of those were retired, last carried at -`2ef07e45`. **Module code is untouched**: rules call +(`run_job_sp_canfar_v2.0.bash → job_sp_canfar_v2.0.bash`) for real data, and +retired the CANFAR submission front end that drove them +(`curl_canfar_local.sh`, `canfar_submit_job`) and the per-site sbatch +reimplementations — those were last carried at `2ef07e45`. The two Gen-2 bash +scripts are still in the tree and still installed (`pyproject.toml`'s +`[tool.setuptools] script-files`): sp_validation's image-simulation workflow +calls `run_job_sp_canfar_v2.0.bash` by path, so they retire when that chain is +ported or parked, not with this one. **Module code is untouched**: rules call `shapepipe_run -c ` on the existing config chains. Design and rationale: [CosmoStat/shapepipe#848](https://github.com/CosmoStat/shapepipe/issues/848) (the living PRD). From 77af69169ba342ac243376b910352895357808a9 Mon Sep 17 00:00:00 2001 From: Cail Daley Date: Sun, 6 Sep 2026 00:31:04 +0200 Subject: [PATCH 36/36] build: regenerate uv.lock from pyproject instead of editing it by hand `uv lock` (uv 0.11.28, the dev image) after dropping skaha. The only package-level change is skaha gone; the rest is uv dropping the redundant `sys_platform == 'linux'` markers, which the next Renovate lockFileMaintenance would do regardless. Replaces the hand edit in 9ee5732b. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01PJjVNvKrQztrZttuAbbUDW --- uv.lock | 1118 +++++++++++++++++++++++++++---------------------------- 1 file changed, 559 insertions(+), 559 deletions(-) diff --git a/uv.lock b/uv.lock index 5c97988f1..ccf54862c 100644 --- a/uv.lock +++ b/uv.lock @@ -14,7 +14,7 @@ name = "accessible-pygments" version = "0.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } wheels = [ @@ -62,8 +62,8 @@ name = "anyio" version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ @@ -75,7 +75,7 @@ name = "argon2-cffi" version = "25.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argon2-cffi-bindings", marker = "sys_platform == 'linux'" }, + { name = "argon2-cffi-bindings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } wheels = [ @@ -87,7 +87,7 @@ name = "argon2-cffi-bindings" version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "sys_platform == 'linux'" }, + { name = "cffi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" } wheels = [ @@ -127,8 +127,8 @@ name = "arrow" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil", marker = "sys_platform == 'linux'" }, - { name = "tzdata", marker = "sys_platform == 'linux'" }, + { name = "python-dateutil" }, + { name = "tzdata" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } wheels = [ @@ -140,11 +140,11 @@ name = "astropy" version = "8.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy-iers-data", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pyerfa", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "astropy-iers-data" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyerfa" }, + { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/c4/21be4313ddfde5f60e0607fd307f367b9e0f0bf153a89b10cbd036dd8cfd/astropy-8.0.1.tar.gz", hash = "sha256:45ca31d5b91fa294cd590a4791a32db94de7f9c8a343155f4d5877baa82351da", size = 7152500, upload-time = "2026-07-05T07:24:48.482Z" } wheels = [ @@ -158,8 +158,8 @@ name = "astropy-healpix" version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/15/c1/aeb3fe3be2ee863708d625014267c71abfe20ddaa293b3d4ddb72ee1d6e9/astropy_healpix-2.0.1.tar.gz", hash = "sha256:0e3f1c94064c45da779900cb90c938df7aef99a924abb23eeb893b16540e77e6", size = 112256, upload-time = "2026-07-20T21:07:30.004Z" } wheels = [ @@ -185,13 +185,13 @@ name = "astroquery" version = "0.4.11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "beautifulsoup4", marker = "sys_platform == 'linux'" }, - { name = "html5lib", marker = "sys_platform == 'linux'" }, - { name = "keyring", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "pyvo", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "beautifulsoup4" }, + { name = "html5lib" }, + { name = "keyring" }, + { name = "numpy" }, + { name = "pyvo" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/48/273dbde090e071f9d264d084bc49193d126498d2906172b78febd9d62e28/astroquery-0.4.11.tar.gz", hash = "sha256:5537529bddc7fa07e773d5cd9baca593e3f5d93474edd1914f68e89506042b33", size = 12561055, upload-time = "2025-09-20T04:26:36.744Z" } wheels = [ @@ -239,8 +239,8 @@ name = "beautifulsoup4" version = "4.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "soupsieve", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "soupsieve" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" } wheels = [ @@ -252,7 +252,7 @@ name = "bleach" version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "webencodings", marker = "sys_platform == 'linux'" }, + { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ @@ -261,7 +261,7 @@ wheels = [ [package.optional-dependencies] css = [ - { name = "tinycss2", marker = "sys_platform == 'linux'" }, + { name = "tinycss2" }, ] [[package]] @@ -269,8 +269,8 @@ name = "build" version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pyproject-hooks", marker = "sys_platform == 'linux'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/b7/1db48a9ce2984842c8c886432ec8a2719613322e868a966ba82a28862f25/build-1.6.0.tar.gz", hash = "sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af", size = 113825, upload-time = "2026-08-27T21:01:16.458Z" } wheels = [ @@ -282,12 +282,12 @@ name = "cadcutils" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "distro", marker = "sys_platform == 'linux'" }, - { name = "lxml", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pyopenssl", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, - { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "distro" }, + { name = "lxml" }, + { name = "packaging" }, + { name = "pyopenssl" }, + { name = "requests" }, + { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/44/4964823c72c9e6a93e26de0118cead37fa3c3600b4677f0c06788c903c80/cadcutils-1.6.2.tar.gz", hash = "sha256:6e2d7822756d48a363bc5f857cd717f80aab760554df86518b957f8826a7a906", size = 94673, upload-time = "2026-06-22T17:35:47.303Z" } wheels = [ @@ -299,10 +299,10 @@ name = "camb" version = "2.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, - { name = "sympy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "scipy" }, + { name = "sympy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/0b/856e8e329c088155509e3c1a999b02cd3f0333a31fc1fa37653455a4ec80/camb-2.0.4.tar.gz", hash = "sha256:2f1f5f3b3964a3746693dc88a54218dca4ea049a04726f8177664c11d2d2616e", size = 925860, upload-time = "2026-08-25T22:10:58.612Z" } wheels = [ @@ -315,19 +315,19 @@ name = "canfar" version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cadcutils", marker = "sys_platform == 'linux'" }, - { name = "click", marker = "sys_platform == 'linux'" }, - { name = "defusedxml", marker = "sys_platform == 'linux'" }, - { name = "httpx", extra = ["http2"], marker = "sys_platform == 'linux'" }, - { name = "humanize", marker = "sys_platform == 'linux'" }, - { name = "pydantic", marker = "sys_platform == 'linux'" }, - { name = "pydantic-settings", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, - { name = "questionary", marker = "sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'linux'" }, - { name = "segno", marker = "sys_platform == 'linux'" }, - { name = "toml", marker = "sys_platform == 'linux'" }, - { name = "typer", marker = "sys_platform == 'linux'" }, + { name = "cadcutils" }, + { name = "click" }, + { name = "defusedxml" }, + { name = "httpx", extra = ["http2"] }, + { name = "humanize" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "rich" }, + { name = "segno" }, + { name = "toml" }, + { name = "typer" }, ] sdist = { url = "https://files.pythonhosted.org/packages/24/d8/a457c57a8c83d6a6a4a96aa41fc46484489a83932bc5fe26180e1daef130/canfar-1.4.1.tar.gz", hash = "sha256:bbd7a37a21e6f9edf8790176797763187fe38a39758c21d652d77ad2fb76a3ee", size = 25743742, upload-time = "2026-06-11T20:39:05.07Z" } wheels = [ @@ -348,7 +348,7 @@ name = "cffi" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy' and sys_platform == 'linux'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } wheels = [ @@ -538,7 +538,7 @@ name = "conda-inject" version = "1.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/a8/8dc86113c65c949cc72d651461d6e4c544b3302a85ed14a5298829e6a419/conda_inject-1.3.2.tar.gz", hash = "sha256:0b8cde8c47998c118d8ff285a04977a3abcf734caf579c520fca469df1cd0aac", size = 3635, upload-time = "2024-05-27T12:20:58.873Z" } wheels = [ @@ -565,7 +565,7 @@ name = "contourpy" version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ @@ -675,7 +675,7 @@ name = "cryptography" version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'linux'" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } wheels = [ @@ -719,18 +719,18 @@ name = "cs-util" version = "0.2.2" source = { git = "https://github.com/CosmoStat/cs_util?branch=develop#1b15a5554e378226d98ae7bfe94e7db05573edf0" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "camb", marker = "sys_platform == 'linux'" }, - { name = "datetime", marker = "sys_platform == 'linux'" }, - { name = "healpy", marker = "sys_platform == 'linux'" }, - { name = "healsparse", marker = "sys_platform == 'linux'" }, - { name = "keyring", marker = "sys_platform == 'linux'" }, - { name = "matplotlib", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "pyccl", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, - { name = "swig", marker = "sys_platform == 'linux'" }, - { name = "vos", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "camb" }, + { name = "datetime" }, + { name = "healpy" }, + { name = "healsparse" }, + { name = "keyring" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pyccl" }, + { name = "scipy" }, + { name = "swig" }, + { name = "vos" }, ] [[package]] @@ -747,13 +747,13 @@ name = "dask" version = "2026.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "sys_platform == 'linux'" }, - { name = "cloudpickle", marker = "sys_platform == 'linux'" }, - { name = "fsspec", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "partd", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, - { name = "toolz", marker = "sys_platform == 'linux'" }, + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, ] sdist = { url = "https://files.pythonhosted.org/packages/33/a7/6b3c7ac32b642fbbe0821111654e0bd8cfbe88f68560bcf23cc78ab35c71/dask-2026.8.0.tar.gz", hash = "sha256:8a94c37b5de6d869343340dc26c3c3acca7ec48a3abdabe00ea3abb1125884d5", size = 11561752, upload-time = "2026-08-24T19:21:25.906Z" } wheels = [ @@ -762,7 +762,7 @@ wheels = [ [package.optional-dependencies] array = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] [[package]] @@ -770,11 +770,11 @@ name = "dask-image" version = "2026.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dask", extra = ["array"], marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "pims", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, - { name = "tifffile", marker = "sys_platform == 'linux'" }, + { name = "dask", extra = ["array"] }, + { name = "numpy" }, + { name = "pims" }, + { name = "scipy" }, + { name = "tifffile" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/49/e592a13a5e1efdcdb8f1faab7c4e309c61648792e276f9ced5fb79381b33/dask_image-2026.5.0.tar.gz", hash = "sha256:ed6b462277e691b2c12b0890ba801a0f9a00cc1894b0aa71b195a7a1419b2b00", size = 80457, upload-time = "2026-05-27T14:05:57.383Z" } wheels = [ @@ -786,8 +786,8 @@ name = "datetime" version = "6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytz", marker = "sys_platform == 'linux'" }, - { name = "zope-interface", marker = "sys_platform == 'linux'" }, + { name = "pytz" }, + { name = "zope-interface" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/32/decbfd165e9985ba9d8c2d34a39afe5aeba2fc3fe390eb6e9ef1aab98fa8/datetime-6.0.tar.gz", hash = "sha256:c1514936d2f901e10c8e08d83bf04e6c9dbd7ca4f244da94fec980980a3bc4d5", size = 64167, upload-time = "2025-11-25T08:00:34.586Z" } wheels = [ @@ -838,7 +838,7 @@ name = "donfig" version = "0.8.1.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/71/80cc718ff6d7abfbabacb1f57aaa42e9c1552bfdd01e64ddd704e4a03638/donfig-0.8.1.post1.tar.gz", hash = "sha256:3bef3413a4c1c601b585e8d297256d0c1470ea012afa6e8461dc28bfb7c23f52", size = 19506, upload-time = "2024-05-23T14:14:31.513Z" } wheels = [ @@ -877,7 +877,7 @@ name = "fitsio" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/fd/53bfe8986f7b242fb5b66ddc9344fe67614102f44438415b0a8b32cf7f0a/fitsio-1.4.2.tar.gz", hash = "sha256:92a02f0e63d539d85ca5a185ae0cc8d40029270858275964ee2539ee0136f0c3", size = 4178786, upload-time = "2026-07-16T12:31:06.723Z" } wheels = [ @@ -948,11 +948,11 @@ name = "galsim" version = "2.8.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "lsstdesc-coord", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "pybind11", marker = "sys_platform == 'linux'" }, - { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "lsstdesc-coord" }, + { name = "numpy" }, + { name = "pybind11" }, + { name = "setuptools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/94/41/358f965c0512531be47c49afe7bdc1ad264b95cf5b9dacb306cad3da82b0/galsim-2.8.5.tar.gz", hash = "sha256:e449bb0d30ece16c3ee7345846b01f0c54e482e37755b623c17174881cfe5ad3", size = 8590974, upload-time = "2026-08-05T03:40:06.337Z" } wheels = [ @@ -971,7 +971,7 @@ name = "gitdb" version = "4.0.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "smmap", marker = "sys_platform == 'linux'" }, + { name = "smmap" }, ] sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ @@ -983,7 +983,7 @@ name = "gitpython" version = "3.1.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "gitdb", marker = "sys_platform == 'linux'" }, + { name = "gitdb" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/61/3285044215fb596bf093e39ccb96ece0a1076a8ca57a61e069a6a33cdb1b/gitpython-3.1.61.tar.gz", hash = "sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95", size = 231680, upload-time = "2026-08-28T11:01:13.761Z" } wheels = [ @@ -1056,8 +1056,8 @@ name = "h2" version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hpack", marker = "sys_platform == 'linux'" }, - { name = "hyperframe", marker = "sys_platform == 'linux'" }, + { name = "hpack" }, + { name = "hyperframe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } wheels = [ @@ -1069,7 +1069,7 @@ name = "h5py" version = "3.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ @@ -1096,8 +1096,8 @@ name = "healpy" version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/54/dd2fa7e6b12f8d3bcc34d61c72cbc1bbbad9b3142583d40803825abea2f6/healpy-1.20.0.tar.gz", hash = "sha256:03b0e1551ae235c7290e9bfdf69fa9cdc194f8a2be51198c1924b15d826102ed", size = 66326598, upload-time = "2026-07-23T20:15:51.281Z" } wheels = [ @@ -1120,8 +1120,8 @@ name = "healsparse" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "hpgeom", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "hpgeom" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/d6/c299485e7bc16779e33824d838395a7339565d193e017ca9aecbfc0a8269/healsparse-1.14.0.tar.gz", hash = "sha256:60e94c8a12ca3af80cfbba7b7b11c30be1c6b5585132dd9f8ea93c7935887198", size = 133778, upload-time = "2026-08-21T04:33:43.11Z" } wheels = [ @@ -1142,7 +1142,7 @@ name = "hpgeom" version = "1.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/72/27640de7d85a566b8ecd27e7894f371a19669c56c268756e18111a137df8/hpgeom-1.5.4.tar.gz", hash = "sha256:85ac73e267c11f3f248920d3541a0e6ffab821cae54f0d44414d1939dfb91200", size = 153906, upload-time = "2026-02-24T19:03:01.954Z" } wheels = [ @@ -1168,8 +1168,8 @@ name = "html5lib" version = "1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "sys_platform == 'linux'" }, - { name = "webencodings", marker = "sys_platform == 'linux'" }, + { name = "six" }, + { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/b6/b55c3f49042f1df3dcd422b7f224f939892ee94f22abcf503a9b7339eaf2/html5lib-1.1.tar.gz", hash = "sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f", size = 272215, upload-time = "2020-06-22T23:32:38.834Z" } wheels = [ @@ -1181,8 +1181,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "sys_platform == 'linux'" }, - { name = "h11", marker = "sys_platform == 'linux'" }, + { name = "certifi" }, + { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -1194,10 +1194,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "sys_platform == 'linux'" }, - { name = "certifi", marker = "sys_platform == 'linux'" }, - { name = "httpcore", marker = "sys_platform == 'linux'" }, - { name = "idna", marker = "sys_platform == 'linux'" }, + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -1206,7 +1206,7 @@ wheels = [ [package.optional-dependencies] http2 = [ - { name = "h2", marker = "sys_platform == 'linux'" }, + { name = "h2" }, ] [[package]] @@ -1241,7 +1241,7 @@ name = "hypothesis" version = "6.167.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sortedcontainers", marker = "sys_platform == 'linux'" }, + { name = "sortedcontainers" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8cee74c1390b2932406faaab76980f18946f258fa5a8afca17189b3bc655/hypothesis-6.167.1.tar.gz", hash = "sha256:62eefcb4d2791423626e9901c3027a6e0c5ffda2ac0b44b3c7e797ab9d2d5a4c", size = 505849, upload-time = "2026-08-30T19:53:09.05Z" } wheels = [ @@ -1291,7 +1291,7 @@ name = "id" version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "urllib3", marker = "sys_platform == 'linux'" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/04/c2156091427636080787aac190019dc64096e56a23b7364d3c1764ee3a06/id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069", size = 18088, upload-time = "2026-02-04T16:19:41.26Z" } wheels = [ @@ -1312,8 +1312,8 @@ name = "imageio" version = "2.37.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, + { name = "pillow" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/62/aa770a9307508d2a2a2c62d536a49347bffe9e55322db27838d3c93d0b07/imageio-2.37.4.tar.gz", hash = "sha256:e45cbc5e83502047fb138f7f585f7f105a136a57eea5f4b3cfc6ce1b52720bd3", size = 390173, upload-time = "2026-07-20T05:26:11.369Z" } wheels = [ @@ -1350,7 +1350,7 @@ name = "importlib-metadata" version = "9.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp", marker = "sys_platform == 'linux'" }, + { name = "zipp" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/7e/1e7e8dc30634b93ebb3d58a3dea569ad146e656218d3960ab04f62047b29/importlib_metadata-9.0.1.tar.gz", hash = "sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99", size = 59124, upload-time = "2026-08-28T15:30:34.646Z" } wheels = [ @@ -1371,18 +1371,18 @@ name = "ipykernel" version = "7.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "comm", marker = "sys_platform == 'linux'" }, - { name = "debugpy", marker = "sys_platform == 'linux'" }, - { name = "ipython", marker = "sys_platform == 'linux'" }, - { name = "jupyter-client", marker = "sys_platform == 'linux'" }, - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "matplotlib-inline", marker = "sys_platform == 'linux'" }, - { name = "nest-asyncio2", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "psutil", marker = "sys_platform == 'linux'" }, - { name = "pyzmq", marker = "sys_platform == 'linux'" }, - { name = "tornado", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio2" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/c4/e4a38f579de4225a561305666f7541cdabb30075def2aa1ac17bd73c1fb5/ipykernel-7.3.0.tar.gz", hash = "sha256:9acaaaf97d16355166e4085afe9d225bfbdf2b7ef520f9df3be8f2b248275e09", size = 184899, upload-time = "2026-06-10T08:41:25.481Z" } wheels = [ @@ -1394,15 +1394,15 @@ name = "ipython" version = "9.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ipython-pygments-lexers", marker = "sys_platform == 'linux'" }, - { name = "jedi", marker = "sys_platform == 'linux'" }, - { name = "matplotlib-inline", marker = "sys_platform == 'linux'" }, - { name = "pexpect", marker = "sys_platform == 'linux'" }, - { name = "prompt-toolkit", marker = "sys_platform == 'linux'" }, - { name = "psutil", marker = "sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'linux'" }, - { name = "stack-data", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect" }, + { name = "prompt-toolkit" }, + { name = "psutil" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c4/bc/e05ae123712ce4e1fde4408eedca1791fc1ff832684565132ea1dc646092/ipython-9.17.0.tar.gz", hash = "sha256:1dc69e6966b270fb259f676c71a21450e63607729b14a672b942914a54e8b730", size = 4538547, upload-time = "2026-08-28T09:00:58.233Z" } wheels = [ @@ -1414,7 +1414,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1426,7 +1426,7 @@ name = "isoduration" version = "20.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "arrow", marker = "sys_platform == 'linux'" }, + { name = "arrow" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } wheels = [ @@ -1438,7 +1438,7 @@ name = "jaraco-classes" version = "3.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "more-itertools", marker = "sys_platform == 'linux'" }, + { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } wheels = [ @@ -1459,7 +1459,7 @@ name = "jaraco-functools" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "more-itertools", marker = "sys_platform == 'linux'" }, + { name = "more-itertools" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6c/1f/c23395957d41ccf27c4e535c3d334c4051e5395b3752057ba4cbaec35c56/jaraco_functools-4.6.0.tar.gz", hash = "sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280", size = 20837, upload-time = "2026-07-14T01:28:02.544Z" } wheels = [ @@ -1471,7 +1471,7 @@ name = "jedi" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "parso", marker = "sys_platform == 'linux'" }, + { name = "parso" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ @@ -1492,7 +1492,7 @@ name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe", marker = "sys_platform == 'linux'" }, + { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ @@ -1531,10 +1531,10 @@ name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "sys_platform == 'linux'" }, - { name = "jsonschema-specifications", marker = "sys_platform == 'linux'" }, - { name = "referencing", marker = "sys_platform == 'linux'" }, - { name = "rpds-py", marker = "sys_platform == 'linux'" }, + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1543,15 +1543,15 @@ wheels = [ [package.optional-dependencies] format-nongpl = [ - { name = "fqdn", marker = "sys_platform == 'linux'" }, - { name = "idna", marker = "sys_platform == 'linux'" }, - { name = "isoduration", marker = "sys_platform == 'linux'" }, - { name = "jsonpointer", marker = "sys_platform == 'linux'" }, - { name = "rfc3339-validator", marker = "sys_platform == 'linux'" }, - { name = "rfc3986-validator", marker = "sys_platform == 'linux'" }, - { name = "rfc3987-syntax", marker = "sys_platform == 'linux'" }, - { name = "uri-template", marker = "sys_platform == 'linux'" }, - { name = "webcolors", marker = "sys_platform == 'linux'" }, + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, ] [[package]] @@ -1559,7 +1559,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing", marker = "sys_platform == 'linux'" }, + { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -1571,8 +1571,8 @@ name = "jupyter-builder" version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/e1/e4ef07e2be228271b59e011a746d97feef69577af1f465b1cff0f1d7c760/jupyter_builder-1.2.2.tar.gz", hash = "sha256:b6cea88f58e44b2c5eba96f28d2e0d16fd453d3ca6dc9c4492ff8a1f2e97f601", size = 981074, upload-time = "2026-08-07T06:47:18.742Z" } wheels = [ @@ -1584,12 +1584,12 @@ name = "jupyter-client" version = "8.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "python-dateutil", marker = "sys_platform == 'linux'" }, - { name = "pyzmq", marker = "sys_platform == 'linux'" }, - { name = "tornado", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/2a/906772148a06e48885039e0250c340b770a55bbf37b08d6ee0449df369c3/jupyter_client-8.10.0.tar.gz", hash = "sha256:9f7116294dca55f1785be880057d44544db9b1567718d92cb33c58886afb9497", size = 360653, upload-time = "2026-08-28T12:17:10.854Z" } wheels = [ @@ -1601,8 +1601,8 @@ name = "jupyter-core" version = "5.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "platformdirs" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } wheels = [ @@ -1614,14 +1614,14 @@ name = "jupyter-events" version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jsonschema", extra = ["format-nongpl"], marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "python-json-logger", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, - { name = "referencing", marker = "sys_platform == 'linux'" }, - { name = "rfc3339-validator", marker = "sys_platform == 'linux'" }, - { name = "rfc3986-validator", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/f8/475c4241b2b75af0deaae453ed003c6c851766dbc44d332d8baf245dc931/jupyter_events-0.12.1.tar.gz", hash = "sha256:faff25f77218335752f35f23c5fe6e4a392a7bd99a5939ccb9b8fbf594636cf3", size = 62854, upload-time = "2026-04-20T23:17:50.66Z" } wheels = [ @@ -1633,7 +1633,7 @@ name = "jupyter-lsp" version = "2.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-server", marker = "sys_platform == 'linux'" }, + { name = "jupyter-server" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/ff/1e4a61f5170a9a1d978f3ac3872449de6c01fc71eaf89657824c878b1549/jupyter_lsp-2.3.1.tar.gz", hash = "sha256:fdf8a4aa7d85813976d6e29e95e6a2c8f752701f926f2715305249a3829805a6", size = 55677, upload-time = "2026-04-02T08:10:06.749Z" } wheels = [ @@ -1645,23 +1645,23 @@ name = "jupyter-server" version = "2.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "sys_platform == 'linux'" }, - { name = "argon2-cffi", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "jupyter-client", marker = "sys_platform == 'linux'" }, - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "jupyter-events", marker = "sys_platform == 'linux'" }, - { name = "jupyter-server-terminals", marker = "sys_platform == 'linux'" }, - { name = "nbconvert", marker = "sys_platform == 'linux'" }, - { name = "nbformat", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "prometheus-client", marker = "sys_platform == 'linux'" }, - { name = "pyzmq", marker = "sys_platform == 'linux'" }, - { name = "send2trash", marker = "sys_platform == 'linux'" }, - { name = "terminado", marker = "sys_platform == 'linux'" }, - { name = "tornado", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, - { name = "websocket-client", marker = "sys_platform == 'linux'" }, + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/e0/63b481d5b21f81fe23173dfc9eb25629fa1bc174fdc4a2c19d09c1ff8124/jupyter_server-2.21.0.tar.gz", hash = "sha256:70d9a1883f57d3576ea17f4ce061ec1a7aad7ef388d00428cfb7f5e4f0022271", size = 760357, upload-time = "2026-08-27T16:06:34.049Z" } wheels = [ @@ -1673,7 +1673,7 @@ name = "jupyter-server-terminals" version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "terminado", marker = "sys_platform == 'linux'" }, + { name = "terminado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } wheels = [ @@ -1685,19 +1685,19 @@ name = "jupyterlab" version = "4.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "async-lru", marker = "sys_platform == 'linux'" }, - { name = "httpx", marker = "sys_platform == 'linux'" }, - { name = "ipykernel", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "jupyter-builder", marker = "sys_platform == 'linux'" }, - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "jupyter-lsp", marker = "sys_platform == 'linux'" }, - { name = "jupyter-server", marker = "sys_platform == 'linux'" }, - { name = "jupyterlab-server", marker = "sys_platform == 'linux'" }, - { name = "notebook-shim", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "tornado", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-builder" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "tornado" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/c0/45934229995d4c6e38192ca118d93185f60808f64157e3df3c5c28f8c5fd/jupyterlab-4.6.3.tar.gz", hash = "sha256:2e3db6e3a12495ebd188276e985bf5ac502fbde3d1e8628819920210008de498", size = 28319771, upload-time = "2026-08-10T18:50:57.947Z" } wheels = [ @@ -1718,13 +1718,13 @@ name = "jupyterlab-server" version = "2.28.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "babel", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "json5", marker = "sys_platform == 'linux'" }, - { name = "jsonschema", marker = "sys_platform == 'linux'" }, - { name = "jupyter-server", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } wheels = [ @@ -1736,11 +1736,11 @@ name = "keyring" version = "25.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jaraco-classes", marker = "sys_platform == 'linux'" }, - { name = "jaraco-context", marker = "sys_platform == 'linux'" }, - { name = "jaraco-functools", marker = "sys_platform == 'linux'" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, - { name = "secretstorage", marker = "sys_platform == 'linux'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney" }, + { name = "secretstorage" }, ] sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } wheels = [ @@ -1848,7 +1848,7 @@ name = "lazy-loader" version = "0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } wheels = [ @@ -1885,8 +1885,8 @@ name = "lsstdesc-coord" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "future" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/e0/6806df4cfa54927a2d8e425407611b4fc031dd125bc952b4a31deb743305/lsstdesc_coord-1.3.1.tar.gz", hash = "sha256:60f878c29e1f30a9b50bf60dca3c466dc9cfb2cbc71f0a27c575ced969de57ab", size = 41647, upload-time = "2026-02-13T18:37:32.611Z" } wheels = [ @@ -1978,7 +1978,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl", marker = "sys_platform == 'linux'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -2028,15 +2028,15 @@ name = "matplotlib" version = "3.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "contourpy", marker = "sys_platform == 'linux'" }, - { name = "cycler", marker = "sys_platform == 'linux'" }, - { name = "fonttools", marker = "sys_platform == 'linux'" }, - { name = "kiwisolver", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pillow", marker = "sys_platform == 'linux'" }, - { name = "pyparsing", marker = "sys_platform == 'linux'" }, - { name = "python-dateutil", marker = "sys_platform == 'linux'" }, + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } wheels = [ @@ -2062,7 +2062,7 @@ name = "matplotlib-inline" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ @@ -2074,11 +2074,11 @@ name = "mccd" version = "1.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "modopt", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "python-pysap", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "modopt" }, + { name = "numpy" }, + { name = "python-pysap" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/b4/2d5618bff92bcc1b928dcc356cbce19cd4ade689d046ba0267da10247f38/mccd-1.2.4.tar.gz", hash = "sha256:e146c84867f4f97e58a0d3de6ead25feceb46f58a1a24e29d102007723e8719f", size = 75762, upload-time = "2023-02-17T19:09:17.039Z" } wheels = [ @@ -2090,7 +2090,7 @@ name = "mdit-py-plugins" version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, + { name = "markdown-it-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ @@ -2120,10 +2120,10 @@ name = "modopt" version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, - { name = "tqdm", marker = "sys_platform == 'linux'" }, + { name = "importlib-metadata" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/cf/7665c4990b8623b9e27733edd17a69c63e2d35c5f05d1a253d5ed2f98e52/modopt-1.7.2.tar.gz", hash = "sha256:d5e8edd935b813c3677beeed3245ef4894e7ac09e180150368eda044914488b4", size = 778069, upload-time = "2024-04-12T10:22:11.24Z" } wheels = [ @@ -2173,12 +2173,12 @@ name = "myst-parser" version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, - { name = "mdit-py-plugins", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'linux'" }, + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } wheels = [ @@ -2199,10 +2199,10 @@ name = "nbclient" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-client", marker = "sys_platform == 'linux'" }, - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "nbformat", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/a5/b3bae4b590c0cbcada2c63a34f7580024e834a8ba213e949a2f906705787/nbclient-0.11.0.tar.gz", hash = "sha256:04a134a5b087f2c5887f228aca155db50169b8cd9334dee6942c8e927e56081a", size = 62535, upload-time = "2026-06-05T07:52:41.746Z" } wheels = [ @@ -2214,20 +2214,20 @@ name = "nbconvert" version = "7.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "beautifulsoup4", marker = "sys_platform == 'linux'" }, - { name = "bleach", extra = ["css"], marker = "sys_platform == 'linux'" }, - { name = "defusedxml", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "jupyterlab-pygments", marker = "sys_platform == 'linux'" }, - { name = "markupsafe", marker = "sys_platform == 'linux'" }, - { name = "mistune", marker = "sys_platform == 'linux'" }, - { name = "nbclient", marker = "sys_platform == 'linux'" }, - { name = "nbformat", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pandocfilters", marker = "sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/01/b1/708e53fe2e429c103c6e6e159106bcf0357ac41aa4c28772bd8402339051/nbconvert-7.17.1.tar.gz", hash = "sha256:34d0d0a7e73ce3cbab6c5aae8f4f468797280b01fd8bd2ca746da8569eddd7d2", size = 865311, upload-time = "2026-04-08T00:44:14.914Z" } wheels = [ @@ -2239,10 +2239,10 @@ name = "nbformat" version = "5.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fastjsonschema", marker = "sys_platform == 'linux'" }, - { name = "jsonschema", marker = "sys_platform == 'linux'" }, - { name = "jupyter-core", marker = "sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'linux'" }, + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/31/72/b3446efab8756e7df4b8ec587f8e611cb5a7249e4323db480802f1d3be04/nbformat-5.11.1.tar.gz", hash = "sha256:32d4521c68c6e7d5b29c76defaeed9f42ea733142b9b19f88277ce10390b9c4d", size = 147775, upload-time = "2026-08-17T08:10:51.942Z" } wheels = [ @@ -2303,9 +2303,9 @@ name = "nibabel" version = "5.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/01/3d2cc510c616bc8e27be17a063070d9126f69407961594a9ae734ea51121/nibabel-5.4.2.tar.gz", hash = "sha256:d5f4b9076a13178ae7f7acf18c8dbd503ee1c4d5c0c23b85df7be87efcbb49da", size = 4663132, upload-time = "2026-03-11T13:31:52.42Z" } wheels = [ @@ -2317,7 +2317,7 @@ name = "notebook-shim" version = "0.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jupyter-server", marker = "sys_platform == 'linux'" }, + { name = "jupyter-server" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } wheels = [ @@ -2329,8 +2329,8 @@ name = "numba" version = "0.67.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "llvmlite", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "llvmlite" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/90/2544f4e3a61e501d6c9a5418fd4b905323222693d54a02cab0106a0af865/numba-0.67.0.tar.gz", hash = "sha256:cd75aa535b33fa05d9d930b1ae8af9f97a2881e96d72dfb38ec9b78284d9f851", size = 2836515, upload-time = "2026-08-11T23:04:00.174Z" } wheels = [ @@ -2349,8 +2349,8 @@ name = "numcodecs" version = "0.16.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/bd/8a391e7c356366224734efd24da929cc4796fff468bfb179fe1af6548535/numcodecs-0.16.5.tar.gz", hash = "sha256:0d0fb60852f84c0bd9543cc4d2ab9eefd37fc8efcc410acd4777e62a1d300318", size = 6276387, upload-time = "2025-11-21T02:49:48.986Z" } wheels = [ @@ -2399,7 +2399,7 @@ name = "numpydoc" version = "1.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", marker = "sys_platform == 'linux'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/3c/dfccc9e7dee357fb2aa13c3890d952a370dd0ed071e0f7ed62ed0df567c1/numpydoc-1.10.0.tar.gz", hash = "sha256:3f7970f6eee30912260a6b31ac72bba2432830cd6722569ec17ee8d3ef5ffa01", size = 94027, upload-time = "2025-12-02T16:39:12.937Z" } wheels = [ @@ -2420,8 +2420,8 @@ name = "pandas" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "python-dateutil", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } wheels = [ @@ -2467,8 +2467,8 @@ name = "partd" version = "1.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "locket", marker = "sys_platform == 'linux'" }, - { name = "toolz", marker = "sys_platform == 'linux'" }, + { name = "locket" }, + { name = "toolz" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } wheels = [ @@ -2480,7 +2480,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform == 'linux'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -2533,11 +2533,11 @@ name = "pims" version = "0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "imageio", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "slicerator", marker = "sys_platform == 'linux'" }, - { name = "tifffile", marker = "sys_platform == 'linux'" }, + { name = "imageio" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "slicerator" }, + { name = "tifffile" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/02/5bf3639f5b77e9b183011c08541c5039ba3d04f5316c70312b48a8e003a9/pims-0.7.tar.gz", hash = "sha256:55907a4c301256086d2aa4e34a5361b9109f24e375c2071e1117b9491e82946b", size = 87779, upload-time = "2024-06-10T19:20:42.842Z" } @@ -2564,7 +2564,7 @@ name = "progressbar2" version = "4.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-utils", marker = "sys_platform == 'linux'" }, + { name = "python-utils" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0c/1e/5bd376abe32a392e74fffd82feb42ee1472d263bd358d628af20663d0346/progressbar2-4.6.0.tar.gz", hash = "sha256:fe48c8955a84428af77bff2642ba47041e1b8f7c867a5b7cc94f8bc255a8f0cf", size = 542464, upload-time = "2026-08-14T01:07:30.008Z" } wheels = [ @@ -2585,7 +2585,7 @@ name = "prompt-toolkit" version = "3.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wcwidth", marker = "sys_platform == 'linux'" }, + { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ @@ -2658,8 +2658,8 @@ name = "pybtex" version = "0.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "latexcodec", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "latexcodec" }, + { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/f5/f30da9c93f0fa6d619332b2f69597219b625f35780473a05164a9981fd9a/pybtex-0.26.1.tar.gz", hash = "sha256:2e5543bea424e60e9e42eef70bff597be48649d8f68ba061a7a092b2477d5464", size = 692991, upload-time = "2026-04-03T13:05:39.014Z" } wheels = [ @@ -2671,8 +2671,8 @@ name = "pybtex-docutils" version = "1.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", marker = "sys_platform == 'linux'" }, - { name = "pybtex", marker = "sys_platform == 'linux'" }, + { name = "docutils" }, + { name = "pybtex" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/84/796ea94d26188a853660f81bded39f8de4cfe595130aef0dea1088705a11/pybtex-docutils-1.0.3.tar.gz", hash = "sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b", size = 18348, upload-time = "2023-08-22T18:47:54.833Z" } wheels = [ @@ -2684,10 +2684,10 @@ name = "pyccl" version = "3.3.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/76/dc091056f1af48e6cb56d651dac6eb8494dd2515547d9543c0ce5365b873/pyccl-3.3.6.tar.gz", hash = "sha256:fd6c0181381eb57345ec9b56bdcf9300c9a31a42014aa9c8ced95f31ace16795", size = 16790489, upload-time = "2026-07-28T09:29:17.738Z" } wheels = [ @@ -2710,10 +2710,10 @@ name = "pydantic" version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types", marker = "sys_platform == 'linux'" }, - { name = "pydantic-core", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, - { name = "typing-inspection", marker = "sys_platform == 'linux'" }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ @@ -2725,7 +2725,7 @@ name = "pydantic-core" version = "2.46.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } wheels = [ @@ -2778,9 +2778,9 @@ name = "pydantic-settings" version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "sys_platform == 'linux'" }, - { name = "python-dotenv", marker = "sys_platform == 'linux'" }, - { name = "typing-inspection", marker = "sys_platform == 'linux'" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ @@ -2792,14 +2792,14 @@ name = "pydata-sphinx-theme" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "accessible-pygments", marker = "sys_platform == 'linux'" }, - { name = "babel", marker = "sys_platform == 'linux'" }, - { name = "beautifulsoup4", marker = "sys_platform == 'linux'" }, - { name = "docutils", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'linux'" }, + { name = "accessible-pygments" }, + { name = "babel" }, + { name = "beautifulsoup4" }, + { name = "docutils" }, + { name = "jinja2" }, + { name = "pygments" }, + { name = "requests" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/8e/add936feaaa9dade7d5b87c6852566d85e518b38ad32224dea32ef958984/pydata_sphinx_theme-0.20.0.tar.gz", hash = "sha256:0da172d41e19a66de875f4002f7054b385372ec65763852193791e658d50bb4a", size = 5004756, upload-time = "2026-07-09T09:09:14.693Z" } wheels = [ @@ -2811,7 +2811,7 @@ name = "pyerfa" version = "2.0.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/39/63cc8291b0cf324ae710df41527faf7d331bce573899199d926b3e492260/pyerfa-2.0.1.5.tar.gz", hash = "sha256:17d6b24fe4846c65d5e7d8c362dcb08199dc63b30a236aedd73875cc83e1f6c0", size = 818430, upload-time = "2024-11-11T15:22:30.852Z" } wheels = [ @@ -2834,8 +2834,8 @@ name = "pyopenssl" version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, + { name = "cryptography" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/e8/7325d258199b159eb2c03fe32107533e2832e70e63f4fb88a6aa00023201/pyopenssl-26.4.0.tar.gz", hash = "sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7", size = 182046, upload-time = "2026-08-01T19:50:50.512Z" } wheels = [ @@ -2865,8 +2865,8 @@ name = "pyqt5" version = "5.15.11" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyqt5-qt5", marker = "sys_platform == 'linux'" }, - { name = "pyqt5-sip", marker = "sys_platform == 'linux'" }, + { name = "pyqt5-qt5" }, + { name = "pyqt5-sip" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/07/c9ed0bd428df6f87183fca565a79fee19fa7c88c7f00a7f011ab4379e77a/PyQt5-5.15.11.tar.gz", hash = "sha256:fda45743ebb4a27b4b1a51c6d8ef455c4c1b5d610c90d2934c7802b5c1557c52", size = 3216775, upload-time = "2024-07-19T08:39:57.756Z" } wheels = [ @@ -2897,8 +2897,8 @@ name = "pyqtgraph" version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "colorama" }, + { name = "numpy" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/32/36/4c242f81fdcbfa4fb62a5645f6af79191f4097a0577bd5460c24f19cc4ef/pyqtgraph-0.14.0-py3-none-any.whl", hash = "sha256:7abb7c3e17362add64f8711b474dffac5e7b0e9245abdf992e9a44119b7aa4f5", size = 1924755, upload-time = "2025-11-16T19:43:22.251Z" }, @@ -2909,10 +2909,10 @@ name = "pytest" version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "iniconfig", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pluggy", marker = "sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ @@ -2924,9 +2924,9 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", marker = "sys_platform == 'linux'" }, - { name = "pluggy", marker = "sys_platform == 'linux'" }, - { name = "pytest", marker = "sys_platform == 'linux'" }, + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ @@ -2938,7 +2938,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "sys_platform == 'linux'" }, + { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -2968,16 +2968,16 @@ name = "python-pysap" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "matplotlib", marker = "sys_platform == 'linux'" }, - { name = "modopt", marker = "sys_platform == 'linux'" }, - { name = "nibabel", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "progressbar2", marker = "sys_platform == 'linux'" }, - { name = "pywavelets", marker = "sys_platform == 'linux'" }, - { name = "scikit-image", marker = "sys_platform == 'linux'" }, - { name = "scikit-learn", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "matplotlib" }, + { name = "modopt" }, + { name = "nibabel" }, + { name = "numpy" }, + { name = "progressbar2" }, + { name = "pywavelets" }, + { name = "scikit-image" }, + { name = "scikit-learn" }, + { name = "scipy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/90/29b10f180876a322c7cfbadd2552efef34374dceb618fdc3f48ef792666e/python_pysap-0.3.0.tar.gz", hash = "sha256:55cdfec05eacc410723f31df754b67728224935aef7d4332b89ef6e90f2aab97", size = 775664, upload-time = "2026-01-06T10:12:58.287Z" } @@ -2986,7 +2986,7 @@ name = "python-utils" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5d/71/ec6665d4ce42ee5a59fffd31a4d5164f92da15ccb8c758dba13d2419ea53/python_utils-4.0.1.tar.gz", hash = "sha256:4e8e8ecaba3862f843a60c1982c99cda23b522f417006a807996a876c18beb8d", size = 43765, upload-time = "2026-08-30T19:26:04.591Z" } wheels = [ @@ -3007,8 +3007,8 @@ name = "pyvo" version = "1.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/35/12c0f4fa0879316837ac56f275942f006f0735e5aedba529b4449dddc36f/pyvo-1.9.1.tar.gz", hash = "sha256:2f26c99af7c32f3c34b919e2d14eaf1a95914176d693fb7769773f3ab0b7999d", size = 2167800, upload-time = "2026-06-11T14:44:01.349Z" } wheels = [ @@ -3020,7 +3020,7 @@ name = "pywavelets" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/75/50581633d199812205ea8cdd0f6d52f12a624886b74bf1486335b67f01ff/pywavelets-1.9.0.tar.gz", hash = "sha256:148d12203377772bea452a59211d98649c8ee4a05eff019a9021853a36babdc8", size = 3938340, upload-time = "2025-08-04T16:20:04.978Z" } wheels = [ @@ -3079,7 +3079,7 @@ name = "pyzmq" version = "27.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy' and sys_platform == 'linux'" }, + { name = "cffi", marker = "implementation_name == 'pypy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/8d/5b3d5631c2f4b4b8862f64cd0c9eb777b5710eeb5125b4be8dd0a200a4c0/pyzmq-27.2.0.tar.gz", hash = "sha256:54d4259d1bfae24ecdb5ca79f7acc2eac6c286a02d6a0ae617797cb45f0726d3", size = 292316, upload-time = "2026-08-20T19:08:21.19Z" } wheels = [ @@ -3109,7 +3109,7 @@ name = "questionary" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "prompt-toolkit", marker = "sys_platform == 'linux'" }, + { name = "prompt-toolkit" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } wheels = [ @@ -3121,9 +3121,9 @@ name = "readme-renderer" version = "46.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", marker = "sys_platform == 'linux'" }, - { name = "nh3", marker = "sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/d7/9309494fad74ee831d4546f69325b5519f37c6dfb2d9ba495db8c6d4f4ca/readme_renderer-46.0.tar.gz", hash = "sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90", size = 38382, upload-time = "2026-08-28T15:18:32.49Z" } wheels = [ @@ -3135,9 +3135,9 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "sys_platform == 'linux'" }, - { name = "rpds-py", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13' and sys_platform == 'linux'" }, + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -3149,16 +3149,16 @@ name = "reproject" version = "0.21.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "astropy-healpix", marker = "sys_platform == 'linux'" }, - { name = "dask", extra = ["array"], marker = "sys_platform == 'linux'" }, - { name = "dask-image", marker = "sys_platform == 'linux'" }, - { name = "fsspec", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "pillow", marker = "sys_platform == 'linux'" }, - { name = "pyavm", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, - { name = "zarr", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "astropy-healpix" }, + { name = "dask", extra = ["array"] }, + { name = "dask-image" }, + { name = "fsspec" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "pyavm" }, + { name = "scipy" }, + { name = "zarr" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cc/44/6fd820ba336484277a91a2f4808b60d6ec0b9f033f588c237e778f20fe89/reproject-0.21.0.tar.gz", hash = "sha256:01ede715a1993c29431f52ff74189ef30f5e7b2e8b4dc88c1b002145a971dc1c", size = 1622661, upload-time = "2026-06-25T15:11:34.886Z" } wheels = [ @@ -3171,10 +3171,10 @@ name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "sys_platform == 'linux'" }, - { name = "charset-normalizer", marker = "sys_platform == 'linux'" }, - { name = "idna", marker = "sys_platform == 'linux'" }, - { name = "urllib3", marker = "sys_platform == 'linux'" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -3186,7 +3186,7 @@ name = "requests-toolbelt" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "requests", marker = "sys_platform == 'linux'" }, + { name = "requests" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } wheels = [ @@ -3198,7 +3198,7 @@ name = "rfc3339-validator" version = "0.1.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "sys_platform == 'linux'" }, + { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } wheels = [ @@ -3228,7 +3228,7 @@ name = "rfc3987-syntax" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "lark", marker = "sys_platform == 'linux'" }, + { name = "lark" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } wheels = [ @@ -3240,8 +3240,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", marker = "sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'linux'" }, + { name = "markdown-it-py" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -3350,14 +3350,14 @@ name = "scikit-image" version = "0.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "imageio", marker = "sys_platform == 'linux'" }, - { name = "lazy-loader", marker = "sys_platform == 'linux'" }, - { name = "networkx", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pillow", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, - { name = "tifffile", marker = "sys_platform == 'linux'" }, + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } wheels = [ @@ -3388,11 +3388,11 @@ name = "scikit-learn" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "joblib", marker = "sys_platform == 'linux'" }, - { name = "narwhals", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "scipy", marker = "sys_platform == 'linux'" }, - { name = "threadpoolctl", marker = "sys_platform == 'linux'" }, + { name = "joblib" }, + { name = "narwhals" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } wheels = [ @@ -3411,7 +3411,7 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -3442,8 +3442,8 @@ name = "secretstorage" version = "3.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "sys_platform == 'linux'" }, - { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "cryptography" }, + { name = "jeepney" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } wheels = [ @@ -3482,9 +3482,9 @@ name = "sf-tools" version = "2.0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future", marker = "sys_platform == 'linux'" }, - { name = "modopt", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "future" }, + { name = "modopt" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/41/6281d12b863867fc7d7db0aeb55db7ec94fb8f35873b9940edeeb3c29d0f/sf_tools-2.0.4.tar.gz", hash = "sha256:607204b7369cb381c02ba95f49f13d2733877a625026fac340dcddcca6700d24", size = 10426, upload-time = "2019-08-22T08:41:46.961Z" } @@ -3493,86 +3493,86 @@ name = "shapepipe" version = "1.1.0" source = { editable = "." } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "astroquery", marker = "sys_platform == 'linux'" }, - { name = "canfar", marker = "sys_platform == 'linux'" }, - { name = "cs-util", marker = "sys_platform == 'linux'" }, - { name = "galsim", marker = "sys_platform == 'linux'" }, - { name = "h5py", marker = "sys_platform == 'linux'" }, - { name = "healsparse", marker = "sys_platform == 'linux'" }, - { name = "hpgeom", marker = "sys_platform == 'linux'" }, - { name = "joblib", marker = "sys_platform == 'linux'" }, - { name = "matplotlib", marker = "sys_platform == 'linux'" }, - { name = "mccd", marker = "sys_platform == 'linux'" }, - { name = "modopt", marker = "sys_platform == 'linux'" }, - { name = "mpi4py", marker = "sys_platform == 'linux'" }, - { name = "ngmix", marker = "sys_platform == 'linux'" }, - { name = "numba", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "pandas", marker = "sys_platform == 'linux'" }, - { name = "pyqt5", marker = "sys_platform == 'linux'" }, - { name = "pyqtgraph", marker = "sys_platform == 'linux'" }, - { name = "python-dateutil", marker = "sys_platform == 'linux'" }, - { name = "python-pysap", marker = "sys_platform == 'linux'" }, - { name = "reproject", marker = "sys_platform == 'linux'" }, - { name = "sf-tools", marker = "sys_platform == 'linux'" }, - { name = "skyproj", marker = "sys_platform == 'linux'" }, - { name = "sqlitedict", marker = "sys_platform == 'linux'" }, - { name = "termcolor", marker = "sys_platform == 'linux'" }, - { name = "tqdm", marker = "sys_platform == 'linux'" }, - { name = "vos", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "astroquery" }, + { name = "canfar" }, + { name = "cs-util" }, + { name = "galsim" }, + { name = "h5py" }, + { name = "healsparse" }, + { name = "hpgeom" }, + { name = "joblib" }, + { name = "matplotlib" }, + { name = "mccd" }, + { name = "modopt" }, + { name = "mpi4py" }, + { name = "ngmix" }, + { name = "numba" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyqt5" }, + { name = "pyqtgraph" }, + { name = "python-dateutil" }, + { name = "python-pysap" }, + { name = "reproject" }, + { name = "sf-tools" }, + { name = "skyproj" }, + { name = "sqlitedict" }, + { name = "termcolor" }, + { name = "tqdm" }, + { name = "vos" }, ] [package.optional-dependencies] dev = [ - { name = "build", marker = "sys_platform == 'linux'" }, - { name = "fitsio", marker = "sys_platform == 'linux'" }, - { name = "hypothesis", marker = "sys_platform == 'linux'" }, - { name = "ipython", marker = "sys_platform == 'linux'" }, - { name = "jupyterlab", marker = "sys_platform == 'linux'" }, - { name = "matplotlib", marker = "sys_platform == 'linux'" }, - { name = "myst-parser", marker = "sys_platform == 'linux'" }, - { name = "numpydoc", marker = "sys_platform == 'linux'" }, - { name = "pytest", marker = "sys_platform == 'linux'" }, - { name = "pytest-cov", marker = "sys_platform == 'linux'" }, - { name = "ruff", marker = "sys_platform == 'linux'" }, - { name = "skyproj", marker = "sys_platform == 'linux'" }, - { name = "snakemake", marker = "sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'linux'" }, - { name = "sphinx-book-theme", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-bibtex", marker = "sys_platform == 'linux'" }, - { name = "twine", marker = "sys_platform == 'linux'" }, + { name = "build" }, + { name = "fitsio" }, + { name = "hypothesis" }, + { name = "ipython" }, + { name = "jupyterlab" }, + { name = "matplotlib" }, + { name = "myst-parser" }, + { name = "numpydoc" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, + { name = "skyproj" }, + { name = "snakemake" }, + { name = "sphinx" }, + { name = "sphinx-book-theme" }, + { name = "sphinxcontrib-bibtex" }, + { name = "twine" }, ] doc = [ - { name = "myst-parser", marker = "sys_platform == 'linux'" }, - { name = "numpydoc", marker = "sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'linux'" }, - { name = "sphinx-book-theme", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-bibtex", marker = "sys_platform == 'linux'" }, + { name = "myst-parser" }, + { name = "numpydoc" }, + { name = "sphinx" }, + { name = "sphinx-book-theme" }, + { name = "sphinxcontrib-bibtex" }, ] fitsio = [ - { name = "fitsio", marker = "sys_platform == 'linux'" }, + { name = "fitsio" }, ] jupyter = [ - { name = "ipython", marker = "sys_platform == 'linux'" }, - { name = "jupyterlab", marker = "sys_platform == 'linux'" }, - { name = "snakemake", marker = "sys_platform == 'linux'" }, + { name = "ipython" }, + { name = "jupyterlab" }, + { name = "snakemake" }, ] lint = [ - { name = "ruff", marker = "sys_platform == 'linux'" }, + { name = "ruff" }, ] plot = [ - { name = "matplotlib", marker = "sys_platform == 'linux'" }, - { name = "skyproj", marker = "sys_platform == 'linux'" }, + { name = "matplotlib" }, + { name = "skyproj" }, ] release = [ - { name = "build", marker = "sys_platform == 'linux'" }, - { name = "twine", marker = "sys_platform == 'linux'" }, + { name = "build" }, + { name = "twine" }, ] test = [ - { name = "hypothesis", marker = "sys_platform == 'linux'" }, - { name = "pytest", marker = "sys_platform == 'linux'" }, - { name = "pytest-cov", marker = "sys_platform == 'linux'" }, + { name = "hypothesis" }, + { name = "pytest" }, + { name = "pytest-cov" }, ] [package.metadata] @@ -3649,11 +3649,11 @@ name = "skyproj" version = "2.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astropy", marker = "sys_platform == 'linux'" }, - { name = "healsparse", marker = "sys_platform == 'linux'" }, - { name = "hpgeom", marker = "sys_platform == 'linux'" }, - { name = "matplotlib", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "astropy" }, + { name = "healsparse" }, + { name = "hpgeom" }, + { name = "matplotlib" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/90/cf/5158151ae6fc60459f430451b612a4bf2da07ebff2a9f52a37e73715a5ee/skyproj-2.5.0.tar.gz", hash = "sha256:cb8d5115927ca43cacdb6d92f00bb4fcc8563ee71028f755203ce1752e67c140", size = 8521587, upload-time = "2026-06-26T19:51:15.378Z" } wheels = [ @@ -3675,7 +3675,7 @@ name = "smart-open" version = "7.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wrapt", marker = "sys_platform == 'linux'" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/c6/22e7a2acd5d27941e85e0d7ede398da5abe2e4677d2265c924157247c32e/smart_open-7.7.1.tar.gz", hash = "sha256:9414ba5733e28309f29b28a303b0f1054ad23fe0275f1a1b600c80a724f4bd1a", size = 54952, upload-time = "2026-06-26T07:56:35.309Z" } wheels = [ @@ -3696,37 +3696,37 @@ name = "snakemake" version = "9.26.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "conda-inject", marker = "sys_platform == 'linux'" }, - { name = "configargparse", marker = "sys_platform == 'linux'" }, - { name = "connection-pool", marker = "sys_platform == 'linux'" }, - { name = "docutils", marker = "sys_platform == 'linux'" }, - { name = "dpath", marker = "sys_platform == 'linux'" }, - { name = "gitpython", marker = "sys_platform == 'linux'" }, - { name = "humanfriendly", marker = "sys_platform == 'linux'" }, - { name = "immutables", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "jsonschema", marker = "sys_platform == 'linux'" }, - { name = "nbformat", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "platformdirs", marker = "sys_platform == 'linux'" }, - { name = "psutil", marker = "sys_platform == 'linux'" }, - { name = "pulp", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, - { name = "referencing", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, - { name = "smart-open", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-executor-plugins", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-logger-plugins", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-report-plugins", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-scheduler-plugins", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-storage-plugins", marker = "sys_platform == 'linux'" }, - { name = "sqlmodel", marker = "sys_platform == 'linux'" }, - { name = "tabulate", marker = "sys_platform == 'linux'" }, - { name = "tenacity", marker = "sys_platform == 'linux'" }, - { name = "throttler", marker = "sys_platform == 'linux'" }, - { name = "wrapt", marker = "sys_platform == 'linux'" }, - { name = "yte", marker = "sys_platform == 'linux'" }, + { name = "conda-inject" }, + { name = "configargparse" }, + { name = "connection-pool" }, + { name = "docutils" }, + { name = "dpath" }, + { name = "gitpython" }, + { name = "humanfriendly" }, + { name = "immutables" }, + { name = "jinja2" }, + { name = "jsonschema" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "psutil" }, + { name = "pulp" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "requests" }, + { name = "smart-open" }, + { name = "snakemake-interface-common" }, + { name = "snakemake-interface-executor-plugins" }, + { name = "snakemake-interface-logger-plugins" }, + { name = "snakemake-interface-report-plugins" }, + { name = "snakemake-interface-scheduler-plugins" }, + { name = "snakemake-interface-storage-plugins" }, + { name = "sqlmodel" }, + { name = "tabulate" }, + { name = "tenacity" }, + { name = "throttler" }, + { name = "wrapt" }, + { name = "yte" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3e/37/f999e99357a9b30c6f36c92027ab3b6f64cade4ec54840197853297d2e04/snakemake-9.26.1.tar.gz", hash = "sha256:091b5d480c0c5eb2ef75b2568e9885091f3e2f11372de358da98ef66ef66e1e0", size = 6814489, upload-time = "2026-08-28T05:44:51.852Z" } wheels = [ @@ -3738,9 +3738,9 @@ name = "snakemake-interface-common" version = "1.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argparse-dataclass", marker = "sys_platform == 'linux'" }, - { name = "configargparse", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, + { name = "argparse-dataclass" }, + { name = "configargparse" }, + { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/c3/592f832f6e5d2d31f749392e48e8401b7625dec668d3d365d8d28f2b6c30/snakemake_interface_common-1.23.0.tar.gz", hash = "sha256:6ed14531a461417659364a0dd0acc51b786af4e26fc15cc5e00ff3d9fcaffacc", size = 13960, upload-time = "2026-03-08T21:54:29.251Z" } wheels = [ @@ -3752,9 +3752,9 @@ name = "snakemake-interface-executor-plugins" version = "9.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argparse-dataclass", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, - { name = "throttler", marker = "sys_platform == 'linux'" }, + { name = "argparse-dataclass" }, + { name = "snakemake-interface-common" }, + { name = "throttler" }, ] sdist = { url = "https://files.pythonhosted.org/packages/54/50/de06b284c45a8e94fb8e4a12d5235065e78b49b8f84329dc10fe39f4b7dd/snakemake_interface_executor_plugins-9.4.0.tar.gz", hash = "sha256:9d4138897beacbaadaedad94b63f948eaeb604b7fc78f9cf65ac57f090f2c066", size = 16549, upload-time = "2026-03-08T17:04:02.644Z" } wheels = [ @@ -3766,7 +3766,7 @@ name = "snakemake-interface-logger-plugins" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-common" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a7/0c/3fa5d592663c65669a867526604aadc6fbc235fb9284e94b49c0ef59aa41/snakemake_interface_logger_plugins-2.1.0.tar.gz", hash = "sha256:c89a00d2a398490cecd91b6dc6db8049cba93712d82e1d8f3000f3040bf3791c", size = 15917, upload-time = "2026-05-20T15:12:35.259Z" } wheels = [ @@ -3778,7 +3778,7 @@ name = "snakemake-interface-report-plugins" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-common" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/d6/6160ed98de665d6871dd356597dbf726688cc786e88668359ca37b7d9f54/snakemake_interface_report_plugins-1.3.0.tar.gz", hash = "sha256:fc9495298bec4e69721ab8afe6d6d88a86966fda2eeb003db56b9a88b86d5934", size = 4283, upload-time = "2025-10-31T10:52:36.55Z" } wheels = [ @@ -3790,7 +3790,7 @@ name = "snakemake-interface-scheduler-plugins" version = "2.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, + { name = "snakemake-interface-common" }, ] sdist = { url = "https://files.pythonhosted.org/packages/88/d9/d480807d2cfc2d132bc760d877d45ec8fbe620a24200ec4d2697c4a26031/snakemake_interface_scheduler_plugins-2.0.2.tar.gz", hash = "sha256:2797e8fa9019d983132c2b403f14d6fcd3c5ad4c8d8a66b984b4740a71cacc46", size = 8642, upload-time = "2025-10-20T13:58:12.988Z" } wheels = [ @@ -3802,11 +3802,11 @@ name = "snakemake-interface-storage-plugins" version = "4.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "humanfriendly", marker = "sys_platform == 'linux'" }, - { name = "snakemake-interface-common", marker = "sys_platform == 'linux'" }, - { name = "tenacity", marker = "sys_platform == 'linux'" }, - { name = "throttler", marker = "sys_platform == 'linux'" }, - { name = "wrapt", marker = "sys_platform == 'linux'" }, + { name = "humanfriendly" }, + { name = "snakemake-interface-common" }, + { name = "tenacity" }, + { name = "throttler" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/6e/f3c5b2d621fd6a6b78d8cfc01fef6b926fe2c277f5ed77c5e4deeacb94eb/snakemake_interface_storage_plugins-4.4.1.tar.gz", hash = "sha256:b2b5bf05318af36955ebf2ce76c921c0fb06904ca98fb30e1657d88b7b7b6945", size = 14924, upload-time = "2026-03-16T11:16:01.075Z" } wheels = [ @@ -3845,22 +3845,22 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alabaster", marker = "sys_platform == 'linux'" }, - { name = "babel", marker = "sys_platform == 'linux'" }, - { name = "docutils", marker = "sys_platform == 'linux'" }, - { name = "imagesize", marker = "sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, - { name = "roman-numerals", marker = "sys_platform == 'linux'" }, - { name = "snowballstemmer", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-applehelp", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-devhelp", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-htmlhelp", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-jsmath", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-qthelp", marker = "sys_platform == 'linux'" }, - { name = "sphinxcontrib-serializinghtml", marker = "sys_platform == 'linux'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -3872,8 +3872,8 @@ name = "sphinx-book-theme" version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydata-sphinx-theme", marker = "sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'linux'" }, + { name = "pydata-sphinx-theme" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/8a/cb008736682b3974cae6925c54939792fe56f5a167e37984cb45d8a85d8a/sphinx_book_theme-1.4.0.tar.gz", hash = "sha256:fee3cd70573d9a8cd4ab380a3cb1f11139c24dcaa2cf47d06d7f924f1b44eaff", size = 406113, upload-time = "2026-07-19T21:23:54.183Z" } wheels = [ @@ -3894,10 +3894,10 @@ name = "sphinxcontrib-bibtex" version = "2.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", marker = "sys_platform == 'linux'" }, - { name = "pybtex", marker = "sys_platform == 'linux'" }, - { name = "pybtex-docutils", marker = "sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'linux'" }, + { name = "docutils" }, + { name = "pybtex" }, + { name = "pybtex-docutils" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/15/6a/8e0b2c2420286389e7fed78ff361ec30e2f1d58c8560af8d64df5e7b61e0/sphinxcontrib_bibtex-2.7.0.tar.gz", hash = "sha256:fee700f7aae29bb8f654c62913f00d34ac44fc0b8ca0fa67ac922ff4453addee", size = 120669, upload-time = "2026-05-06T09:29:24.935Z" } wheels = [ @@ -3954,8 +3954,8 @@ name = "sqlalchemy" version = "2.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "greenlet", marker = "(platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'WIN32' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'amd64' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 'win32' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } wheels = [ @@ -3985,8 +3985,8 @@ name = "sqlmodel" version = "0.0.37" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "sys_platform == 'linux'" }, - { name = "sqlalchemy", marker = "sys_platform == 'linux'" }, + { name = "pydantic" }, + { name = "sqlalchemy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fb/26/1d2faa0fd5a765267f49751de533adac6b9ff9366c7c6e7692df4f32230f/sqlmodel-0.0.37.tar.gz", hash = "sha256:d2c19327175794faf50b1ee31cc966764f55b1dedefc046450bc5741a3d68352", size = 85527, upload-time = "2026-02-21T16:39:47.038Z" } wheels = [ @@ -3998,9 +3998,9 @@ name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asttokens", marker = "sys_platform == 'linux'" }, - { name = "executing", marker = "sys_platform == 'linux'" }, - { name = "pure-eval", marker = "sys_platform == 'linux'" }, + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ @@ -4034,7 +4034,7 @@ name = "sympy" version = "1.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mpmath", marker = "sys_platform == 'linux'" }, + { name = "mpmath" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ @@ -4073,8 +4073,8 @@ name = "terminado" version = "0.18.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "os_name != 'nt' and sys_platform == 'linux'" }, - { name = "tornado", marker = "sys_platform == 'linux'" }, + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "tornado" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } wheels = [ @@ -4104,7 +4104,7 @@ name = "tifffile" version = "2026.8.23" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "numpy" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/07/90078f49e60718d414d5440dc498301f11ff049458e65cf8d27c62a5c9d1/tifffile-2026.8.23.tar.gz", hash = "sha256:bd3c816f166f85c93329a54a0c9a1eccc9968a6a78f91d63b65d7b17675915f2", size = 446179, upload-time = "2026-08-23T18:45:05.976Z" } wheels = [ @@ -4116,7 +4116,7 @@ name = "tinycss2" version = "1.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "webencodings", marker = "sys_platform == 'linux'" }, + { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" } wheels = [ @@ -4176,15 +4176,15 @@ name = "twine" version = "6.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "id", marker = "sys_platform == 'linux'" }, - { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x' and sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "readme-renderer", marker = "sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'linux'" }, - { name = "requests-toolbelt", marker = "sys_platform == 'linux'" }, - { name = "rfc3986", marker = "sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'linux'" }, - { name = "urllib3", marker = "sys_platform == 'linux'" }, + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } wheels = [ @@ -4196,9 +4196,9 @@ name = "typer" version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-doc", marker = "sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'linux'" }, - { name = "shellingham", marker = "sys_platform == 'linux'" }, + { name = "annotated-doc" }, + { name = "rich" }, + { name = "shellingham" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/f7/57713ba479fd405eb76de31404b2c744c289e336b2d999511ebf51e496f7/typer-0.27.2.tar.gz", hash = "sha256:269b7eb9d3c202ca84b4bc9618cb04ebb43d3d4d1e567e4c768607232c05f945", size = 204045, upload-time = "2026-08-28T10:26:55.046Z" } wheels = [ @@ -4219,7 +4219,7 @@ name = "typing-inspection" version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ @@ -4258,9 +4258,9 @@ name = "vos" version = "3.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aenum", marker = "sys_platform == 'linux'" }, - { name = "cadcutils", marker = "sys_platform == 'linux'" }, - { name = "html2text", marker = "sys_platform == 'linux'" }, + { name = "aenum" }, + { name = "cadcutils" }, + { name = "html2text" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2f/71/35b07ebde34232b4f3deca8cb320c3ad615b15818fbb3b10a5209c14aadc/vos-3.7.tar.gz", hash = "sha256:073cde0ec3ffe52f283ba6505870e760492a73743986797686e7dc57d30ca563", size = 103538, upload-time = "2026-08-06T23:47:46.099Z" } wheels = [ @@ -4353,9 +4353,9 @@ name = "yte" version = "1.9.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "argparse-dataclass", marker = "sys_platform == 'linux'" }, - { name = "dpath", marker = "sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'linux'" }, + { name = "argparse-dataclass" }, + { name = "dpath" }, + { name = "pyyaml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/f5/7e44620e6e077bfe624b9a17c329b8e0d0159e176e1f1a93c2790428ab2c/yte-1.9.4.tar.gz", hash = "sha256:86a47e6d722cec9419a7ac88be57d0d6c4ce28f02860393b71a66f2c674069f6", size = 8101, upload-time = "2025-11-27T12:55:00.85Z" } wheels = [ @@ -4367,12 +4367,12 @@ name = "zarr" version = "3.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "donfig", marker = "sys_platform == 'linux'" }, - { name = "google-crc32c", marker = "sys_platform == 'linux'" }, - { name = "numcodecs", marker = "sys_platform == 'linux'" }, - { name = "numpy", marker = "sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'linux'" }, + { name = "donfig" }, + { name = "google-crc32c" }, + { name = "numcodecs" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/15/436cb1d3bbe86173bd44ce7a34ecb210d0c0416946e337858149a905ef5a/zarr-3.3.0.tar.gz", hash = "sha256:cd0c8cf738b4bb4807815bc1255acad5bdf1a7b7264b606c5a1bc0d0392a306b", size = 943626, upload-time = "2026-07-30T16:35:10.491Z" } wheels = [