From 059409c80b8c1dcf6a2d3e9a673af9150caf0791 Mon Sep 17 00:00:00 2001 From: Graham Hukill Date: Tue, 7 Apr 2026 13:40:03 -0400 Subject: [PATCH 1/6] Update dependencies --- uv.lock | 800 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 413 insertions(+), 387 deletions(-) diff --git a/uv.lock b/uv.lock index 7f451a5..361158c 100644 --- a/uv.lock +++ b/uv.lock @@ -22,11 +22,11 @@ wheels = [ [[package]] name = "attrs" -version = "25.4.0" +version = "26.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] [[package]] @@ -40,29 +40,29 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.68" +version = "1.42.84" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/ae/60c642aa5413e560b671da825329f510b29a77274ed0f580bde77562294d/boto3-1.42.68.tar.gz", hash = "sha256:3f349f967ab38c23425626d130962bcb363e75f042734fe856ea8c5a00eef03c", size = 112761, upload-time = "2026-03-13T19:32:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/89/2d647bd717da55a8cc68602b197f53a5fa36fb95a2f9e76c4aff11a9cfd1/boto3-1.42.84.tar.gz", hash = "sha256:6a84b3293a5d8b3adf827a54588e7dcffcf0a85410d7dadca615544f97d27579", size = 112816, upload-time = "2026-04-06T19:39:07.585Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/f6/dc6e993479dbb597d68223fbf61cb026511737696b15bd7d2a33e9b2c24f/boto3-1.42.68-py3-none-any.whl", hash = "sha256:dbff353eb7dc93cbddd7926ed24793e0174c04adbe88860dfa639568442e4962", size = 140556, upload-time = "2026-03-13T19:32:14.951Z" }, + { url = "https://files.pythonhosted.org/packages/2d/31/cdf4326841613d1d181a77b3038a988800fb3373ca50de1639fba9fa87de/boto3-1.42.84-py3-none-any.whl", hash = "sha256:4d03ad3211832484037337292586f71f48707141288d9ac23049c04204f4ab03", size = 140555, upload-time = "2026-04-06T19:39:06.009Z" }, ] [[package]] name = "boto3-stubs" -version = "1.42.68" +version = "1.42.84" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/8c/dd4b0c95ff008bed5a35ab411452ece121b355539d2a0b6dcd62a0c47be5/boto3_stubs-1.42.68.tar.gz", hash = "sha256:96ad1020735619483fb9b4da7a5e694b460bf2e18f84a34d5d175d0ffe8c4653", size = 101372, upload-time = "2026-03-13T19:49:54.867Z" } +sdist = { url = "https://files.pythonhosted.org/packages/97/7a/5c7cd6b60345592319ea2db2ee4147ff7b2b25456ffd2cf05e29c293cb9e/boto3_stubs-1.42.84.tar.gz", hash = "sha256:c517c254e1d8f00af24f7df55c8b1061d1142405c5ac07e426ee2b5b709f3362", size = 102186, upload-time = "2026-04-06T20:07:17.77Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/15/3ca5848917214a168134512a5b45f856a56e913659888947a052e02031b5/boto3_stubs-1.42.68-py3-none-any.whl", hash = "sha256:ed7f98334ef7b2377fa8532190e63dc2c6d1dc895e3d7cb3d6d1c83771b81bf6", size = 70011, upload-time = "2026-03-13T19:49:42.801Z" }, + { url = "https://files.pythonhosted.org/packages/27/af/90281a0333536548d9cb2d452116ae27e2e16a26096fcc999666e172f6e5/boto3_stubs-1.42.84-py3-none-any.whl", hash = "sha256:73c3f47fc18e27dfe6f17c1c4d3ee48ab6f926d1b7029d15e6771c8255a6f278", size = 70448, upload-time = "2026-04-06T20:07:15.265Z" }, ] [package.optional-dependencies] @@ -72,16 +72,16 @@ s3 = [ [[package]] name = "botocore" -version = "1.42.68" +version = "1.42.84" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/22/87502d5fbbfa8189406a617b30b1e2a3dc0ab2669f7268e91b385c1c1c7a/botocore-1.42.68.tar.gz", hash = "sha256:3951c69e12ac871dda245f48dac5c7dd88ea1bfdd74a8879ec356cf2874b806a", size = 14994514, upload-time = "2026-03-13T19:32:03.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/b7/1c03423843fb0d1795b686511c00ee63fed1234c2400f469aeedfd42212f/botocore-1.42.84.tar.gz", hash = "sha256:234064604c80d9272a5e9f6b3566d260bcaa053a5e05246db90d7eca1c2cf44b", size = 15148615, upload-time = "2026-04-06T19:38:56.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/2a/1428f6594799780fe6ee845d8e6aeffafe026cd16a70c878684e2dcbbfc8/botocore-1.42.68-py3-none-any.whl", hash = "sha256:9df7da26374601f890e2f115bfa573d65bf15b25fe136bb3aac809f6145f52ab", size = 14668816, upload-time = "2026-03-13T19:31:58.572Z" }, + { url = "https://files.pythonhosted.org/packages/e3/37/0c0c90361c8a1b9e6c75222ca24ae12996a298c0e18822a72ab229c37207/botocore-1.42.84-py3-none-any.whl", hash = "sha256:15f3fe07dfa6545e46a60c4b049fe2bdf63803c595ae4a4eec90e8f8172764f3", size = 14827061, upload-time = "2026-04-06T19:38:53.613Z" }, ] [[package]] @@ -191,71 +191,87 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/b6/9ee9c1a608916ca5feae81a344dffbaa53b26b90be58cc2159e3332d44ec/charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade", size = 280976, upload-time = "2026-03-06T06:01:15.276Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d8/a54f7c0b96f1df3563e9190f04daf981e365a9b397eedfdfb5dbef7e5c6c/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54", size = 189356, upload-time = "2026-03-06T06:01:16.511Z" }, - { url = "https://files.pythonhosted.org/packages/42/69/2bf7f76ce1446759a5787cb87d38f6a61eb47dbbdf035cfebf6347292a65/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467", size = 206369, upload-time = "2026-03-06T06:01:17.853Z" }, - { url = "https://files.pythonhosted.org/packages/10/9c/949d1a46dab56b959d9a87272482195f1840b515a3380e39986989a893ae/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60", size = 203285, upload-time = "2026-03-06T06:01:19.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/5c/ae30362a88b4da237d71ea214a8c7eb915db3eec941adda511729ac25fa2/charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d", size = 196274, upload-time = "2026-03-06T06:01:20.728Z" }, - { url = "https://files.pythonhosted.org/packages/b2/07/c9f2cb0e46cb6d64fdcc4f95953747b843bb2181bda678dc4e699b8f0f9a/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e", size = 184715, upload-time = "2026-03-06T06:01:22.194Z" }, - { url = "https://files.pythonhosted.org/packages/36/64/6b0ca95c44fddf692cd06d642b28f63009d0ce325fad6e9b2b4d0ef86a52/charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f", size = 193426, upload-time = "2026-03-06T06:01:23.795Z" }, - { url = "https://files.pythonhosted.org/packages/50/bc/a730690d726403743795ca3f5bb2baf67838c5fea78236098f324b965e40/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc", size = 191780, upload-time = "2026-03-06T06:01:25.053Z" }, - { url = "https://files.pythonhosted.org/packages/97/4f/6c0bc9af68222b22951552d73df4532b5be6447cee32d58e7e8c74ecbb7b/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95", size = 185805, upload-time = "2026-03-06T06:01:26.294Z" }, - { url = "https://files.pythonhosted.org/packages/dd/b9/a523fb9b0ee90814b503452b2600e4cbc118cd68714d57041564886e7325/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a", size = 208342, upload-time = "2026-03-06T06:01:27.55Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/c59e761dee4464050713e50e27b58266cc8e209e518c0b378c1580c959ba/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac", size = 193661, upload-time = "2026-03-06T06:01:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/1c/43/729fa30aad69783f755c5ad8649da17ee095311ca42024742701e202dc59/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1", size = 204819, upload-time = "2026-03-06T06:01:30.298Z" }, - { url = "https://files.pythonhosted.org/packages/87/33/d9b442ce5a91b96fc0840455a9e49a611bbadae6122778d0a6a79683dd31/charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98", size = 198080, upload-time = "2026-03-06T06:01:31.478Z" }, - { url = "https://files.pythonhosted.org/packages/56/5a/b8b5a23134978ee9885cee2d6995f4c27cc41f9baded0a9685eabc5338f0/charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262", size = 132630, upload-time = "2026-03-06T06:01:33.056Z" }, - { url = "https://files.pythonhosted.org/packages/70/53/e44a4c07e8904500aec95865dc3f6464dc3586a039ef0df606eb3ac38e35/charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636", size = 142856, upload-time = "2026-03-06T06:01:34.489Z" }, - { url = "https://files.pythonhosted.org/packages/ea/aa/c5628f7cad591b1cf45790b7a61483c3e36cf41349c98af7813c483fd6e8/charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02", size = 132982, upload-time = "2026-03-06T06:01:35.641Z" }, - { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, - { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, - { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, - { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, - { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, - { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, - { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, - { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, - { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, - { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, - { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, - { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, - { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, - { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, - { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, - { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, - { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, - { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, - { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, - { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, - { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, - { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.3.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, ] [[package]] @@ -269,86 +285,86 @@ wheels = [ [[package]] name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [[package]] @@ -367,60 +383,60 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "46.0.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, ] [[package]] name = "cyclonedx-python-lib" -version = "11.6.0" +version = "11.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "license-expression" }, @@ -429,9 +445,9 @@ dependencies = [ { name = "sortedcontainers" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/ed/54ecfa25fc145c58bf4f98090f7b6ffe5188d0759248c57dde44427ea239/cyclonedx_python_lib-11.6.0.tar.gz", hash = "sha256:7fb85a4371fa3a203e5be577ac22b7e9a7157f8b0058b7448731474d6dea7bf0", size = 1408147, upload-time = "2025-12-02T12:28:46.446Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/0d/64f02d3fd9c116d6f50a540d04d1e4f2e3c487f5062d2db53733ddb25917/cyclonedx_python_lib-11.7.0.tar.gz", hash = "sha256:fb1bc3dedfa31208444dbd743007f478ab6984010a184e5bd466bffd969e936e", size = 1411174, upload-time = "2026-03-17T15:19:16.606Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/1b/534ad8a5e0f9470522811a8e5a9bc5d328fb7738ba29faf357467a4ef6d0/cyclonedx_python_lib-11.6.0-py3-none-any.whl", hash = "sha256:94f4aae97db42a452134dafdddcfab9745324198201c4777ed131e64c8380759", size = 511157, upload-time = "2025-12-02T12:28:44.158Z" }, + { url = "https://files.pythonhosted.org/packages/30/09/fe0e3bc32bd33707c519b102fc064ad2a2ce5a1b53e2be38b86936b476b1/cyclonedx_python_lib-11.7.0-py3-none-any.whl", hash = "sha256:02fa4f15ddbba21ac9093039f8137c0d1813af7fe88b760c5dcd3311a8da2178", size = 513041, upload-time = "2026-03-17T15:19:14.369Z" }, ] [[package]] @@ -463,31 +479,31 @@ wheels = [ [[package]] name = "duckdb" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/11/e05a7eb73a373d523e45d83c261025e02bc31ebf868e6282c30c4d02cc59/duckdb-1.5.0.tar.gz", hash = "sha256:f974b61b1c375888ee62bc3125c60ac11c4e45e4457dd1bb31a8f8d3cf277edd", size = 17981141, upload-time = "2026-03-09T12:50:26.372Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/73/120e673e48ae25aaf689044c25ef51b0ea1d088563c9a2532612aea18e0a/duckdb-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9ea988d1d5c8737720d1b2852fd70e4d9e83b1601b8896a1d6d31df5e6afc7dd", size = 30057869, upload-time = "2026-03-09T12:49:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/21/e9/61143471958d36d3f3e764cb4cd43330be208ddbff1c78d3310b9ee67fe8/duckdb-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb786d5472afc16cc3c7355eb2007172538311d6f0cc6f6a0859e84a60220375", size = 15963092, upload-time = "2026-03-09T12:49:17.478Z" }, - { url = "https://files.pythonhosted.org/packages/4f/71/76e37c9a599ad89dd944e6cbb3e6a8ad196944a421758e83adea507637b6/duckdb-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc92b238f4122800a7592e99134124cc9048c50f766c37a0778dd2637f5cbe59", size = 14220562, upload-time = "2026-03-09T12:49:23.518Z" }, - { url = "https://files.pythonhosted.org/packages/db/b8/de1831656d5d13173e27c79c7259c8b9a7bdc314fdc8920604838ea4c46d/duckdb-1.5.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b74cb205c21d3696d8f8b88adca401e1063d6e6f57c1c4f56a243610b086e30", size = 19245329, upload-time = "2026-03-09T12:49:26.307Z" }, - { url = "https://files.pythonhosted.org/packages/1f/8d/33d349a3bcbd3e9b7b4e904c19d5b97f058c4c20791b89a8d6323bb93dce/duckdb-1.5.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e56c19ffd1ffe3642fa89639e71e2e00ab0cf107b62fe16e88030acaebcbde6", size = 21348041, upload-time = "2026-03-09T12:49:30.283Z" }, - { url = "https://files.pythonhosted.org/packages/e2/ec/591a4cad582fae04bc8f8b4a435eceaaaf3838cf0ca771daae16a3c2995b/duckdb-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:86525e565ec0c43420106fd34ba2c739a54c01814d476c7fed3007c9ed6efd86", size = 13053781, upload-time = "2026-03-09T12:49:33.574Z" }, - { url = "https://files.pythonhosted.org/packages/db/62/42e0a13f9919173bec121c0ff702406e1cdd91d8084c3e0b3412508c3891/duckdb-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:5faeebc178c986a7bfa68868a023001137a95a1110bf09b7356442a4eae0f7e7", size = 13862906, upload-time = "2026-03-09T12:49:36.598Z" }, - { url = "https://files.pythonhosted.org/packages/35/5d/af5501221f42e4e3662c047ecec4dcd0761229fceeba3c67ad4d9d8741df/duckdb-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11dd05b827846c87f0ae2f67b9ae1d60985882a7c08ce855379e4a08d5be0e1d", size = 30057396, upload-time = "2026-03-09T12:49:39.95Z" }, - { url = "https://files.pythonhosted.org/packages/43/bd/a278d73fedbd3783bf9aedb09cad4171fe8e55bd522952a84f6849522eb6/duckdb-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ad8d9c91b7c280ab6811f59deff554b845706c20baa28c4e8f80a95690b252b", size = 15962700, upload-time = "2026-03-09T12:49:43.504Z" }, - { url = "https://files.pythonhosted.org/packages/76/fc/c916e928606946209c20fb50898dabf120241fb528a244e2bd8cde1bd9e2/duckdb-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0ee4dabe03ed810d64d93927e0fd18cd137060b81ee75dcaeaaff32cbc816656", size = 14220272, upload-time = "2026-03-09T12:49:46.867Z" }, - { url = "https://files.pythonhosted.org/packages/53/07/1390e69db922423b2e111e32ed342b3e8fad0a31c144db70681ea1ba4d56/duckdb-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409ed1184b363ddea239609c5926f5148ee412b8d9e5ffa617718d755d942f6", size = 19244401, upload-time = "2026-03-09T12:49:49.865Z" }, - { url = "https://files.pythonhosted.org/packages/54/13/b58d718415cde993823a54952ea511d2612302f1d2bc220549d0cef752a4/duckdb-1.5.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1df8c4f9c853a45f3ec1e79ed7fe1957a203e5ec893bbbb853e727eb93e0090f", size = 21345827, upload-time = "2026-03-09T12:49:52.977Z" }, - { url = "https://files.pythonhosted.org/packages/e0/96/4460429651e371eb5ff745a4790e7fa0509c7a58c71fc4f0f893404c9646/duckdb-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a3d3dfa2d8bc74008ce3ad9564761ae23505a9e4282f6a36df29bd87249620b", size = 13053101, upload-time = "2026-03-09T12:49:56.134Z" }, - { url = "https://files.pythonhosted.org/packages/ba/54/6d5b805113214b830fa3c267bb3383fb8febaa30760d0162ef59aadb110a/duckdb-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:2deebcbafd9d39c04f31ec968f4dd7cee832c021e10d96b32ab0752453e247c8", size = 13865071, upload-time = "2026-03-09T12:49:59.282Z" }, - { url = "https://files.pythonhosted.org/packages/66/9f/dd806d4e8ecd99006eb240068f34e1054533da1857ad06ac726305cd102d/duckdb-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:d4b618de670cd2271dd7b3397508c7b3c62d8ea70c592c755643211a6f9154fa", size = 30065704, upload-time = "2026-03-09T12:50:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/79/c2/7b7b8a5c65d5535c88a513e267b5e6d7a55ab3e9b67e4ddd474454653268/duckdb-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:065ae50cb185bac4b904287df72e6b4801b3bee2ad85679576dd712b8ba07021", size = 15964883, upload-time = "2026-03-09T12:50:06.343Z" }, - { url = "https://files.pythonhosted.org/packages/23/c5/9a52a2cdb228b8d8d191a603254364d929274d9cc7d285beada8f7daa712/duckdb-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6be5e48e287a24d98306ce9dd55093c3b105a8fbd8a2e7a45e13df34bf081985", size = 14221498, upload-time = "2026-03-09T12:50:10.567Z" }, - { url = "https://files.pythonhosted.org/packages/b8/68/646045cb97982702a8a143dc2e45f3bdcb79fbe2d559a98d74b8c160e5e2/duckdb-1.5.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5ee41a0bf793882f02192ce105b9a113c3e8c505a27c7ef9437d7b756317113", size = 19249787, upload-time = "2026-03-09T12:50:13.524Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/5abf0c7f38febb3b4a231c784223fceccfd3f2bfd957699d786f46e41ce6/duckdb-1.5.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8e42aaf3cd217417c5dc9ff522dc3939d18b25a6fe5f846348277e831e6f59c", size = 21351583, upload-time = "2026-03-09T12:50:16.701Z" }, - { url = "https://files.pythonhosted.org/packages/93/a4/a90f2901cc0a1ce7ca4f0564b8492b9dbfe048a6395b27933d46ae9be473/duckdb-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:11ae50aaeda2145b50294ee0247e4f11fb9448b3cc3d2aea1cfc456637dfb977", size = 13575130, upload-time = "2026-03-09T12:50:19.716Z" }, - { url = "https://files.pythonhosted.org/packages/64/aa/f14dd5e241ec80d9f9d82196ca65e0c53badfc8a7a619d5497c5626657ad/duckdb-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:d6d2858c734d1a7e7a1b6e9b8403b3fce26dfefb4e0a2479c420fba6cd36db36", size = 14341879, upload-time = "2026-03-09T12:50:22.347Z" }, +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/62/590caabec6c41003f46a244b6fd707d35ca2e552e0c70cbf454e08bf6685/duckdb-1.5.1.tar.gz", hash = "sha256:b370d1620a34a4538ef66524fcee9de8171fa263c701036a92bc0b4c1f2f9c6d", size = 17995082, upload-time = "2026-03-23T12:12:15.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/06/be4c62f812c6e23898733073ace0482eeb18dffabe0585d63a3bf38bca1e/duckdb-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6f7361d66cc801d9eb4df734b139cd7b0e3c257a16f3573ebd550ddb255549e6", size = 30113703, upload-time = "2026-03-23T12:11:02.536Z" }, + { url = "https://files.pythonhosted.org/packages/44/03/1794dcdda75ff203ab0982ff7eb5232549b58b9af66f243f1b7212d6d6be/duckdb-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6acc2040bec1f05de62a2f3f68f4c12f3ec7d6012b4317d0ab1a195af26225", size = 15991802, upload-time = "2026-03-23T12:11:06.321Z" }, + { url = "https://files.pythonhosted.org/packages/87/03/293bccd838a293d42ea26dec7f4eb4f58b57b6c9ffcfabc6518a5f20a24a/duckdb-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed6d23a3f806898e69c77430ebd8da0c79c219f97b9acbc9a29a653e09740c59", size = 14246803, upload-time = "2026-03-23T12:11:09.624Z" }, + { url = "https://files.pythonhosted.org/packages/15/2c/7b4f11879aa2924838168b4640da999dccda1b4a033d43cb998fd6dc33ea/duckdb-1.5.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6af347debc8b721aa72e48671166282da979d5e5ae52dbc660ab417282b48e23", size = 19271654, upload-time = "2026-03-23T12:11:13.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d6/8f9a6b1fbcc669108ec6a4d625a70be9e480b437ed9b70cd56b78cd577a6/duckdb-1.5.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8150c569b2aa4573b51ba8475e814aa41fd53a3d510c1ffb96f1139f46faf611", size = 21386100, upload-time = "2026-03-23T12:11:16.758Z" }, + { url = "https://files.pythonhosted.org/packages/c4/fe/8d02c6473273468cf8d43fd5d73c677f8cdfcd036c1e884df0613f124c2b/duckdb-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:054ad424b051b334052afac58cb216f3b1ebb8579fc8c641e60f0182e8725ea9", size = 13083506, upload-time = "2026-03-23T12:11:19.785Z" }, + { url = "https://files.pythonhosted.org/packages/96/0b/2be786b9c153eb263bf5d3d5f7ab621b14a715d7e70f92b24ecf8536369e/duckdb-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:6ba302115f63f6482c000ccfd62efdb6c41d9d182a5bcd4a90e7ab8cd13856eb", size = 13888862, upload-time = "2026-03-23T12:11:22.84Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f2/af476945e3b97417945b0f660b5efa661863547c0ea104251bb6387342b1/duckdb-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:26e56b5f0c96189e3288d83cf7b476e23615987902f801e5788dee15ee9f24a9", size = 30113759, upload-time = "2026-03-23T12:11:26.5Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9d/5a542b3933647369e601175190093597ce0ac54909aea0dd876ec51ffad4/duckdb-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:972d0dbf283508f9bc446ee09c3838cb7c7f114b5bdceee41753288c97fe2f7c", size = 15991463, upload-time = "2026-03-23T12:11:30.025Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/b59cff67f5e0420b8f337ad86406801cffacae219deed83961dcceefda67/duckdb-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:482f8a13f2600f527e427f73c42b5aa75536f9892868068f0aaf573055a0135f", size = 14246482, upload-time = "2026-03-23T12:11:33.33Z" }, + { url = "https://files.pythonhosted.org/packages/e9/12/d72a82fe502aae82b97b481bf909be8e22db5a403290799ad054b4f90eb4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da137802688190835b4c863cafa77fd7e29dff662ee6d905a9ffc14f00299c91", size = 19270816, upload-time = "2026-03-23T12:11:36.79Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c3/ee49319b15f139e04c067378f0e763f78336fbab38ba54b0852467dd9da4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d4147422d91ccdc2d2abf6ed24196025e020259d1d267970ae20c13c2ce84b1", size = 21385695, upload-time = "2026-03-23T12:11:40.465Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f5/a15498e75a27a136c791ca1889beade96d388dadf9811375db155fc96d1a/duckdb-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:05fc91767d0cfc4cf2fa68966ab5b479ac07561752e42dd0ae30327bd160f64a", size = 13084065, upload-time = "2026-03-23T12:11:43.763Z" }, + { url = "https://files.pythonhosted.org/packages/93/81/b3612d2bbe237f75791095e16767c61067ea5d31c76e8591c212dac13bd0/duckdb-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:a28531cee2a5a42d89f9ba4da53bfeb15681f12acc0263476c8705380dadce07", size = 13892892, upload-time = "2026-03-23T12:11:47.222Z" }, + { url = "https://files.pythonhosted.org/packages/ad/75/e9e7893542ca738bcde2d41d459e3438950219c71c57ad28b049dc2ae616/duckdb-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:eba81e0b3011c1f23df7ea47ef4ffaa8239817959ae291515b6efd068bde2161", size = 30123677, upload-time = "2026-03-23T12:11:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/df/db/f7420ee7109a922124c02f377ae1c56156e9e4aa434f4726848adaef0219/duckdb-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:afab8b4b1f4469c3879bb049dd039f8fce402712050324e9524a43d7324c5e87", size = 15996808, upload-time = "2026-03-23T12:11:54.964Z" }, + { url = "https://files.pythonhosted.org/packages/df/57/2c4c3de1f1110417592741863ba58b4eca2f7690a421712762ddbdcd72e6/duckdb-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:71dddcebbc5a70e946a06c30b59b5dd7999c9833d307168f90fb4e4b672ab63e", size = 14248990, upload-time = "2026-03-23T12:11:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e173b33ffac53124a3e39e97fb60a538f26651a0df6e393eb9bf7540126c/duckdb-1.5.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac2804043bd1bc10b5da18f8f4c706877197263a510c41be9b4c0062f5783dcc", size = 19276013, upload-time = "2026-03-23T12:12:02.034Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4c/47e838393aa90d3d78549c8c04cb09452efeb14aaae0ee24dc0bd61c3a41/duckdb-1.5.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8843bd9594e1387f1e601439e19ad73abdf57356104fd1e53a708255bb95a13d", size = 21387569, upload-time = "2026-03-23T12:12:05.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9b/ce65743e0e85f5c984d2f7e8a81bc908d0bac345d6d8b6316436b29430e7/duckdb-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:d68c5a01a283cb13b79eafe016fe5869aa11bff8c46e7141c70aa0aac808010f", size = 13603876, upload-time = "2026-03-23T12:12:09.344Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ac/f9e4e731635192571f86f52d86234f537c7f8ca4f6917c56b29051c077ef/duckdb-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:a3be2072315982e232bfe49c9d3db0a59ba67b2240a537ef42656cc772a887c7", size = 14370790, upload-time = "2026-03-23T12:12:12.497Z" }, ] [[package]] @@ -563,11 +579,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.17" +version = "2.6.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/84/376a3b96e5a8d33a7aa2c5b3b31a4b3c364117184bf0b17418055f6ace66/identify-2.6.17.tar.gz", hash = "sha256:f816b0b596b204c9fdf076ded172322f2723cf958d02f9c3587504834c8ff04d", size = 99579, upload-time = "2026-03-01T20:04:12.702Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/66/71c1227dff78aaeb942fed29dd5651f2aec166cc7c9aeea3e8b26a539b7d/identify-2.6.17-py2.py3-none-any.whl", hash = "sha256:be5f8412d5ed4b20f2bd41a65f920990bdccaa6a4a18a08f1eefdcd0bdd885f0", size = 99382, upload-time = "2026-03-01T20:04:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" }, ] [[package]] @@ -590,7 +606,7 @@ wheels = [ [[package]] name = "ipython" -version = "9.11.0" +version = "9.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -604,9 +620,9 @@ dependencies = [ { name = "stack-data" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, + { url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" }, ] [[package]] @@ -888,7 +904,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.19.1" +version = "1.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -896,36 +912,46 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, - { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, - { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, - { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, - { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, - { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, - { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, - { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, - { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, - { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, - { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, - { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, - { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, + { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, + { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" }, + { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" }, + { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" }, + { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" }, + { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" }, + { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, ] [[package]] name = "mypy-boto3-s3" -version = "1.42.67" +version = "1.42.80" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/b3/d2cdd49f272add9a178a1a238f6bdd80f0e6b503506b002e727ba699f23a/mypy_boto3_s3-1.42.67.tar.gz", hash = "sha256:3a3a918a9949f2d6f8071d490b8968ddce634aa19590697537e5189cbdca403e", size = 76415, upload-time = "2026-03-12T20:02:08.476Z" } +sdist = { url = "https://files.pythonhosted.org/packages/81/4e/f0f2c0fca253daf2db6230f96899dd0b3b7b592d99f60f07e5b3cc058584/mypy_boto3_s3-1.42.80.tar.gz", hash = "sha256:f3c1d19e8f7346402e341d0395d492b67801f80614bad5d7fa8b5f4db5e4b65d", size = 76540, upload-time = "2026-03-31T19:37:26.133Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/a5/7d4a7bb51c7bb9b188f306555bfcf5537c7f0524964350ff9d04d86e0786/mypy_boto3_s3-1.42.67-py3-none-any.whl", hash = "sha256:93208799734611da4caa5fa8f5ce677b62758ddcd34b737b9f7ae471d179b95e", size = 83570, upload-time = "2026-03-12T20:02:04.391Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/aa321332f62f6c8ce174ad3c3edab6235c0ab36c77dae930fea9e41e69bd/mypy_boto3_s3-1.42.80-py3-none-any.whl", hash = "sha256:23d59dee7650c18c62ebbdc96b11bbfd8b1547341f46c88d1aa8d140276a4276", size = 83751, upload-time = "2026-03-31T19:37:22.112Z" }, ] [[package]] @@ -948,63 +974,63 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, - { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, - { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, - { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, - { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, - { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, - { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, - { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, - { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, - { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, - { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, - { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, - { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, - { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, - { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, - { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, - { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, ] [[package]] @@ -1311,11 +1337,11 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +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" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { 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" }, ] [[package]] @@ -1369,15 +1395,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.1.3" +version = "1.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d7/7e/9f3b0dd3a074a6c3e1e79f35e465b1f2ee4b262d619de00cfce523cc9b24/python_discovery-1.1.3.tar.gz", hash = "sha256:7acca36e818cd88e9b2ba03e045ad7e93e1713e29c6bbfba5d90202310b7baa5", size = 56945, upload-time = "2026-03-10T15:08:15.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/80/73211fc5bfbfc562369b4aa61dc1e4bf07dc7b34df7b317e4539316b809c/python_discovery-1.1.3-py3-none-any.whl", hash = "sha256:90e795f0121bc84572e737c9aa9966311b9fde44ffb88a5953b3ec9b31c6945e", size = 31485, upload-time = "2026-03-10T15:08:13.06Z" }, + { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" }, ] [[package]] @@ -1437,7 +1463,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -1445,9 +1471,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -1479,27 +1505,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, - { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, - { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, - { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, - { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, - { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, - { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, - { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, - { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, - { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, - { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, - { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, ] [[package]] @@ -1586,7 +1612,7 @@ wheels = [ [[package]] name = "timdex-dataset-api" -version = "4.1.0" +version = "5.0.0" source = { editable = "." } dependencies = [ { name = "attrs" }, @@ -1643,47 +1669,47 @@ dev = [ [[package]] name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] [[package]] @@ -1748,11 +1774,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, ] [[package]] @@ -1790,14 +1816,14 @@ wheels = [ [[package]] name = "werkzeug" -version = "3.1.6" +version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, + { url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" }, ] [[package]] From 0969d66cd6e090218ef6b44e72809e4594851911 Mon Sep 17 00:00:00 2001 From: Graham Hukill Date: Tue, 7 Apr 2026 16:23:18 -0400 Subject: [PATCH 2/6] Major refactor for first class data sources Why these changes are being introduced: This library was originally created to handle a single data source, TIMDEX records. Eventually, a second data source of embeddings for records was added. The decision was made at that time to kind of "bolt on" the embeddings into the data model to continue forward progress and return to standardizing their handling in this library later. This is that time, as we look to add a new data source, fulltexts for records. Before going into more details, its imperative to point out that the *physical* structure of records and embeddings parquet files, and soon fulltexts, shared an identical pattern in the parquet dataset. This means all differences were entirely in code, in this library's data model for reading and writing. Before the refactor, "records" were an implied data source on the base `TIMDEXDataset` class. They utilized a metadata query + join to parquet files for memory efficient and performant querying. By contrast, the later added embeddings were a more naive approach that effectively scanned the embeddings parquet files directly. Not only did records and embeddings therefore have different read patterns, there was considerable duplication in reading and writing methods. Moreover, only embeddings had their own dedicated class `TIMDEXEmbeddings`, further entrenching that it was somehow not a "core" data source. With the desire to add fulltexts as a new data source in the TIMDEX dataset, it was decided that now was the proper time to take the learnings from records and embeddings handling and standardize the data model in this library. How this addresses that need: Big picture, - A new base class `TIMDEXDataSource` is created that contains the vast majority of all read, write, and column definitions for a data source. - A data source class `TIMDEXRecords` is created that captures the uniqueness of the original TIMDEX records as a `TIMDEXDataSource` type, and nothing more. The pre-existing class `TIMDEXEmbeddings` is slimmed down to match `TIMDEXRecords` as another `TIMDEXDataSource` type. Both modules are only ~200 lines now, with every line dedicated to that data source specifically. All shared behavior is found in the parent `TIMDEXDataSource` class now. - With the creation of `TIMDEXDataSource`, capturing all read/write business logic, the `TIMDEXDataset` class finally becomes a true orchestration class, representing the dataset as a whole. Similar to how `TIMDEXDatasetMetadata` was composed as `self.metadata`, so too is `TIMDEXRecords` and `TIMDEXEmbeddings`. All read and write mechanics for both of those data sources are at `self.records` and `self.embeddings` respectively, though its important to point out they largely route to the same underlying `TIMDEXDataSource` methods. - `TIMDEXDatasetMetadata` becomes data source agnostic. It continues to provide the heavy lifting of reading and writing dataset metadata, but relies heavily on data source classes like `TIMDEXRecords` and `TIMDEXEmbeddings` to enuemrate their precise schemas and metadatda columns. More editorial, these changes represent the original designs for the TIMDEX dataset: a data lake. We know that all data sources will in some way relate to TIMDEX records, but the physical layout of the parquet files are identical and lend themselves to more equitable treatment in the data model than we previously had. We might now look at `TIMDEXDataset`, `TIMDEXDatasetMetadata`, and `TIMDEXDataSource` as truly data source type agnostic classes that together provide a data lake API. With those in place, the data source classes `TIMDEXRecords` and `TIMDEXEmbeddings` (and soon, `TIMDEXFulltexts`) are data sources that populate the lake. Why a single commit? The work began as an exploratory project to see how we might add fulltexts as a sibling data source to records and embeddings. It quickly became clear that doing so via the previous data model would entrench those patterns and compound technical debt. But to be sure this was a viable path, the work continued to see through the refactor, until this final product emerged. Breaking this up into smaller commits or PRs retrospectively, while theoretically possible, felt like it was ultimately less authentic to the change. Everything was lifted up, inspected, and changed at once. Side effects of this change: - All applications that use TDA will need to update their read and write method calls: - Transmogrifier: will need to change `timdex_dataset.write()` to `timdex_dataset.records.write()` - pipeline lambdas: likely unchanged, as it queried metadata directly - TIM: change `timdex_dataset.read()` to `timdex_dataset.records.read()` - timdex-embeddings: change `timdex_dataset.read()` to `timdex_dataset.records.read()`; it was already using `timdex_dataset.embeddings.write()` - Metadata for each dataset (dev, stage, prod) will need to be rebuilt. Thankfully, this is a very quick, very lightweight action that is designed to happen from time-to-time as needed (this is not the first) Other than that, side effects should be minimal to none. The interface for reading and writing, besides being on `.records` or `.embeddings`, are the same. Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/USE-496 --- pyproject.toml | 2 +- tests/conftest.py | 31 +- tests/test_dataset.py | 164 ++----- tests/test_embeddings.py | 302 +++++++++--- tests/test_metadata.py | 637 +++++++++++++++++++++++-- tests/test_read.py | 107 +++-- tests/test_records.py | 2 +- tests/test_write.py | 126 +++-- timdex_dataset_api/__init__.py | 14 +- timdex_dataset_api/data_source.py | 586 +++++++++++++++++++++++ timdex_dataset_api/dataset.py | 528 +-------------------- timdex_dataset_api/embeddings.py | 568 ++++++---------------- timdex_dataset_api/metadata.py | 754 ++++++++++++++++++++---------- timdex_dataset_api/record.py | 69 --- timdex_dataset_api/records.py | 199 ++++++++ timdex_dataset_api/utils.py | 11 + 16 files changed, 2489 insertions(+), 1611 deletions(-) create mode 100644 timdex_dataset_api/data_source.py delete mode 100644 timdex_dataset_api/record.py create mode 100644 timdex_dataset_api/records.py diff --git a/pyproject.toml b/pyproject.toml index c3fa714..cd7848f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "timdex_dataset_api" -version = "4.1.0" +version = "5.0.0" description = "Python library for interacting with a TIMDEX parquet dataset" readme = "README.md" requires-python = ">=3.12" diff --git a/tests/conftest.py b/tests/conftest.py index d2ad0f4..b7bc2f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ TIMDEXEmbeddings, ) from timdex_dataset_api.metadata import TIMDEXDatasetMetadata -from timdex_dataset_api.record import DatasetRecord +from timdex_dataset_api.records import DatasetRecord @pytest.fixture(autouse=True) @@ -81,7 +81,7 @@ def timdex_dataset(tmp_path, timdex_dataset_config) -> TIMDEXDataset: dataset = TIMDEXDataset( str(tmp_path / "basic_dataset/"), config=timdex_dataset_config ) - dataset.write( + dataset.records.write( generate_sample_records( num_records=1000, source="alma", @@ -111,7 +111,7 @@ def timdex_dataset_multi_source(tmp_path_factory) -> TIMDEXDataset: ("libguides", "jkl123"), ("gismit", "mno456"), ]: - dataset.write( + dataset.records.write( generate_sample_records( num_records=1000, source=source, @@ -170,7 +170,7 @@ def timdex_dataset_with_runs( for num_records, source, run_date, run_type, action, run_id in ( alma_runs + dspace_runs ): - dataset.write( + dataset.records.write( generate_sample_records( num_records=num_records, source=source, @@ -208,7 +208,7 @@ def timdex_dataset_same_day_runs(tmp_path) -> TIMDEXDataset: ] for num_records, source, run_date, run_type, action, run_id, run_timestamp in runs: - dataset.write( + dataset.records.write( generate_sample_records( num_records=num_records, source=source, @@ -268,7 +268,7 @@ def timdex_metadata_with_deltas( action="index", run_id="run-delta-1", ) - td.write(records) + td.records.write(records) # return fresh TIMDEXDataset's metadata return TIMDEXDataset(timdex_dataset_with_runs.location).metadata @@ -281,7 +281,7 @@ def timdex_metadata_merged_deltas( """TIMDEXDatasetMetadata after merging append deltas to static database file.""" # copy directory of a dataset with runs dataset_location = str(tmp_path / "cloned_dataset_with_runs/") - shutil.copytree(timdex_metadata_with_deltas.location, dataset_location) + shutil.copytree(timdex_metadata_with_deltas.timdex_dataset.location, dataset_location) # clone dataset with runs using new dataset location td = TIMDEXDataset(dataset_location, config=timdex_dataset_with_runs.config) @@ -306,11 +306,11 @@ def timdex_embeddings_with_runs(timdex_dataset_empty) -> TIMDEXEmbeddings: timdex_dataset = timdex_dataset_empty # write matching records for embeddings - timdex_dataset.write( + timdex_dataset.records.write( generate_sample_records(100, source="alma", run_id="abc123"), write_append_deltas=False, ) - timdex_dataset.write( + timdex_dataset.records.write( generate_sample_records(50, source="alma", run_id="def456"), write_append_deltas=False, ) @@ -327,7 +327,8 @@ def timdex_embeddings_with_runs(timdex_dataset_empty) -> TIMDEXEmbeddings: generate_sample_embeddings_for_run(timdex_dataset, run_id="def456") ) - # reload TIMDEXDataset instance once more + # rebuild metadata to include embeddings, then reload + timdex_dataset.metadata.rebuild_dataset_metadata() return TIMDEXDataset(timdex_dataset_empty.location).embeddings @@ -343,7 +344,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset: timdex_dataset_dataset = timdex_dataset_empty # scenario 1: apple - single full run - timdex_dataset_dataset.write( + timdex_dataset_dataset.records.write( generate_sample_records( num_records=10, source="apple", @@ -355,7 +356,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset: ) # scenario 2: orange - full run + daily run - timdex_dataset_dataset.write( + timdex_dataset_dataset.records.write( generate_sample_records( num_records=10, source="orange", @@ -365,7 +366,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset: ), write_append_deltas=False, ) - timdex_dataset_dataset.write( + timdex_dataset_dataset.records.write( generate_sample_records( num_records=5, source="orange", @@ -377,7 +378,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset: ) # scenario 3: lemon - full run + daily run (daily will be embedded twice) - timdex_dataset_dataset.write( + timdex_dataset_dataset.records.write( generate_sample_records( num_records=10, source="lemon", @@ -387,7 +388,7 @@ def timdex_dataset_for_embeddings_views(timdex_dataset_empty) -> TIMDEXDataset: ), write_append_deltas=False, ) - timdex_dataset_dataset.write( + timdex_dataset_dataset.records.write( generate_sample_records( num_records=5, source="lemon", diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 8457fb8..7c14d6e 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -1,9 +1,8 @@ -# ruff: noqa: D205, D209, SLF001, PLR2004 +# ruff: noqa: SLF001, PLR2004 import glob import os from datetime import date -from pathlib import Path from unittest.mock import MagicMock, patch import pyarrow as pa @@ -17,9 +16,11 @@ ) -def test_dataset_init_success(tmp_path): +def test_dataset_parse_location_local_uses_local_filesystem(tmp_path): timdex_dataset = TIMDEXDataset(str(tmp_path / "path/to/dataset")) - assert isinstance(timdex_dataset.dataset.filesystem, fs.LocalFileSystem) + filesystem, path = timdex_dataset.parse_location(timdex_dataset.records.data_root) + assert isinstance(filesystem, fs.LocalFileSystem) + assert path.endswith("/data/records") def test_dataset_init_env_vars_set_config(monkeypatch, tmp_path): @@ -42,116 +43,27 @@ def test_dataset_init_custom_config_object(monkeypatch, tmp_path): assert timdex_dataset.config.max_rows_per_file == 42 -@patch("timdex_dataset_api.dataset.fs.LocalFileSystem") -@patch("timdex_dataset_api.dataset.ds.dataset") -def test_load_pyarrow_dataset_default_uses_data_records_root( - mock_pyarrow_ds, mock_local_fs, tmp_path -): - """Ensure load_pyarrow_dataset() without args calls pyarrow.dataset with the - dataset's data_records_root path as the source and the proper filesystem.""" - mock_local_fs.return_value = MagicMock() - mock_pyarrow_ds.return_value = MagicMock() - - location = str(Path(tmp_path) / "local/path/to/default_dataset") - - timdex_dataset = TIMDEXDataset(location=location) - # call the explicit loader to exercise the code path - dataset_obj = timdex_dataset.load_pyarrow_dataset() - - mock_pyarrow_ds.assert_called_with( - f"{location}/data/records", - schema=timdex_dataset.schema, - format="parquet", - partitioning="hive", - filesystem=mock_local_fs.return_value, - ) - assert dataset_obj == mock_pyarrow_ds.return_value - assert timdex_dataset.dataset == mock_pyarrow_ds.return_value - - -@patch("timdex_dataset_api.dataset.fs.LocalFileSystem") -@patch("timdex_dataset_api.dataset.ds.dataset") -def test_load_pyarrow_dataset_with_parquet_files_list( - mock_pyarrow_ds, mock_local_fs, tmp_path -): - """Ensure load_pyarrow_dataset(parquet_files=...) passes the explicit list - of parquet files as the source to pyarrow.dataset.""" - mock_local_fs.return_value = MagicMock() - mock_pyarrow_ds.return_value = MagicMock() - - location = str(Path(tmp_path) / "local/path/to/dataset_with_files") - - timdex_dataset = TIMDEXDataset(location=location) - - parquet_files = [ - f"{timdex_dataset.data_records_root}/source=alma/run_date=2024-12-01/part-0.parquet", - f"{timdex_dataset.data_records_root}/source=alma/run_date=2024-12-01/part-1.parquet", - ] - - dataset_obj = timdex_dataset.load_pyarrow_dataset(parquet_files=parquet_files) - - mock_pyarrow_ds.assert_called_with( - parquet_files, - schema=timdex_dataset.schema, - format="parquet", - partitioning="hive", - filesystem=mock_local_fs.return_value, - ) - assert dataset_obj == mock_pyarrow_ds.return_value - assert timdex_dataset.dataset == mock_pyarrow_ds.return_value - - -@patch("timdex_dataset_api.dataset.fs.LocalFileSystem") -@patch("timdex_dataset_api.dataset.ds.dataset") -def test_dataset_load_local_sets_filesystem_and_dataset_success( - mock_pyarrow_ds, mock_local_fs, tmp_path -): - mock_local_fs.return_value = MagicMock() - mock_pyarrow_ds.return_value = MagicMock() - - location = str(Path(tmp_path) / "local/path/to/dataset") - - timdex_dataset = TIMDEXDataset(location=location) - - mock_pyarrow_ds.assert_called_once_with( - f"{location}/data/records", - schema=timdex_dataset.schema, - format="parquet", - partitioning="hive", - filesystem=mock_local_fs.return_value, - ) - - assert timdex_dataset.dataset == mock_pyarrow_ds.return_value - - @patch("timdex_dataset_api.dataset.TIMDEXDataset.get_s3_filesystem") -@patch("timdex_dataset_api.dataset.ds.dataset") -def test_dataset_load_s3_sets_filesystem_and_dataset_success( - mock_pyarrow_ds, mock_get_s3_fs, s3_bucket_mocked +def test_dataset_parse_location_s3_sets_filesystem_and_path( + mock_get_s3_fs, s3_bucket_mocked ): mock_get_s3_fs.return_value = MagicMock() - mock_pyarrow_ds.return_value = MagicMock() timdex_dataset = TIMDEXDataset(location="s3://timdex/path/to/dataset") + filesystem, path = timdex_dataset.parse_location(timdex_dataset.records.data_root) - mock_pyarrow_ds.assert_called_with( - "timdex/path/to/dataset/data/records", - schema=timdex_dataset.schema, - format="parquet", - partitioning="hive", - filesystem=mock_get_s3_fs.return_value, - ) - assert timdex_dataset.dataset == mock_pyarrow_ds.return_value + assert filesystem == mock_get_s3_fs.return_value + assert path == "timdex/path/to/dataset/data/records" def test_filters_single_nonpartition_success(timdex_dataset_multi_source): - df = timdex_dataset_multi_source.read_dataframe(run_id="abc123") + df = timdex_dataset_multi_source.records.read_dataframe(run_id="abc123") assert df is not None assert set(df["run_id"].unique().tolist()) == {"abc123"} def test_filters_multi_nonpartition_success(timdex_dataset_multi_source): - df = timdex_dataset_multi_source.read_dataframe( + df = timdex_dataset_multi_source.records.read_dataframe( timdex_record_id="alma:0", source="alma", run_type="daily", @@ -164,30 +76,36 @@ def test_filters_multi_nonpartition_success(timdex_dataset_multi_source): def test_filters_or_nonpartition_success(timdex_dataset_multi_source): - df = timdex_dataset_multi_source.read_dataframe(timdex_record_id=["alma:0", "alma:1"]) + df = timdex_dataset_multi_source.records.read_dataframe( + timdex_record_id=["alma:0", "alma:1"] + ) assert df is not None assert set(df["timdex_record_id"].tolist()) == {"alma:0", "alma:1"} def test_filters_run_date_str_success(timdex_dataset_multi_source): - df = timdex_dataset_multi_source.read_dataframe(run_date="2024-12-01") + df = timdex_dataset_multi_source.records.read_dataframe(run_date="2024-12-01") assert df is not None - df_empty = timdex_dataset_multi_source.read_dataframe(run_date="2024-12-02") + df_empty = timdex_dataset_multi_source.records.read_dataframe(run_date="2024-12-02") assert df_empty is None or len(df_empty) == 0 def test_filters_run_date_obj_success(timdex_dataset_multi_source): - df = timdex_dataset_multi_source.read_dataframe(run_date=date(2024, 12, 1)) + df = timdex_dataset_multi_source.records.read_dataframe(run_date=date(2024, 12, 1)) assert df is not None - df_empty = timdex_dataset_multi_source.read_dataframe(run_date=date(2024, 12, 2)) + df_empty = timdex_dataset_multi_source.records.read_dataframe( + run_date=date(2024, 12, 2) + ) assert df_empty is None or len(df_empty) == 0 def test_filters_ymd_success(timdex_dataset_multi_source): # metadata filters do not expose partition y/m/d; use run_date equivalents - df = timdex_dataset_multi_source.read_dataframe(run_date=date(2024, 12, 1)) + df = timdex_dataset_multi_source.records.read_dataframe(run_date=date(2024, 12, 1)) assert df is not None - df_empty = timdex_dataset_multi_source.read_dataframe(run_date=date(2025, 12, 1)) + df_empty = timdex_dataset_multi_source.records.read_dataframe( + run_date=date(2025, 12, 1) + ) assert df_empty is None or len(df_empty) == 0 @@ -195,7 +113,7 @@ def test_filters_run_date_invalid_raise_error(timdex_dataset_multi_source): with pytest.raises( ConversionException, match="Conversion Error: Unimplemented type for cast" ): - timdex_dataset_multi_source.read_dataframe(run_date=999) + timdex_dataset_multi_source.records.read_dataframe(run_date=999) def test_dataset_get_s3_filesystem_success(mocker): @@ -211,35 +129,27 @@ def test_dataset_get_s3_filesystem_success(mocker): assert isinstance(s3_filesystem, pa._s3fs.S3FileSystem) -def test_dataset_timdex_dataset_validate_success(timdex_dataset): - assert timdex_dataset.dataset.to_table().validate() is None # where None is valid - - -def test_dataset_timdex_dataset_row_count_success(timdex_dataset): - assert timdex_dataset.dataset.count_rows() == timdex_dataset.dataset.count_rows() - - def test_dataset_all_records_not_current_and_not_deduped( timdex_dataset_with_runs_with_metadata, ): - all_records_df = timdex_dataset_with_runs_with_metadata.read_dataframe() + all_records_df = timdex_dataset_with_runs_with_metadata.records.read_dataframe() # assert counts reflect all records from dataset, no deduping assert all_records_df.source.value_counts().to_dict() == {"alma": 254, "dspace": 194} # assert run_date min/max dates align with min/max for all runs - assert all_records_df.run_date.min() == date(2024, 12, 1) - assert all_records_df.run_date.max() == date(2025, 2, 5) + assert all_records_df.run_date.min().date() == date(2024, 12, 1) + assert all_records_df.run_date.max().date() == date(2025, 2, 5) def test_dataset_records_data_structure_is_idempotent(timdex_dataset_with_runs): - assert os.path.exists(timdex_dataset_with_runs.data_records_root) - start_file_count = glob.glob(f"{timdex_dataset_with_runs.data_records_root}/**/*") + assert os.path.exists(timdex_dataset_with_runs.records.data_root) + start_file_count = glob.glob(f"{timdex_dataset_with_runs.records.data_root}/**/*") - timdex_dataset_with_runs.create_data_structure() + timdex_dataset_with_runs.records.create_data_structure() - assert os.path.exists(timdex_dataset_with_runs.data_records_root) - end_file_count = glob.glob(f"{timdex_dataset_with_runs.data_records_root}/**/*") + assert os.path.exists(timdex_dataset_with_runs.records.data_root) + end_file_count = glob.glob(f"{timdex_dataset_with_runs.records.data_root}/**/*") assert start_file_count == end_file_count @@ -247,7 +157,7 @@ def test_dataset_duckdb_context_created_on_init(timdex_dataset): assert isinstance(timdex_dataset.conn, DuckDBPyConnection) -def test_dataset_duckdb_context_creates_data_schema(timdex_dataset): +def test_dataset_duckdb_context_does_not_create_data_schema(timdex_dataset): assert ( timdex_dataset.conn.query(""" select count(*) @@ -255,16 +165,14 @@ def test_dataset_duckdb_context_creates_data_schema(timdex_dataset): where catalog_name = 'memory' and schema_name = 'data'; """).fetchone()[0] - == 1 + == 0 ) def test_dataset_preload_current_records_default_false(timdex_dataset): assert timdex_dataset.preload_current_records is False - assert timdex_dataset.metadata.preload_current_records is False def test_dataset_preload_current_records_flag_true(tmp_path): td = TIMDEXDataset(str(tmp_path), preload_current_records=True) assert td.preload_current_records is True - assert td.metadata.preload_current_records is True diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index 62364d5..9bcb149 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -1,26 +1,18 @@ # ruff: noqa: PLR2004 import json -import math import os -from datetime import UTC, date, datetime +from datetime import UTC, datetime import pandas as pd import pyarrow as pa import pyarrow.dataset as ds import pytest -from tests.utils import generate_sample_embeddings_for_run -from timdex_dataset_api.embeddings import ( - METADATA_SELECT_FILTER_COLUMNS, - TIMDEX_DATASET_EMBEDDINGS_SCHEMA, - DatasetEmbedding, - TIMDEXEmbeddings, -) +from tests.utils import generate_sample_embeddings_for_run, generate_sample_records +from timdex_dataset_api import TIMDEXDataset +from timdex_dataset_api.embeddings import DatasetEmbedding, TIMDEXEmbeddings -EMBEDDINGS_COLUMNS_SET = set(TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names) -EMBEDDINGS_WITH_METADATA_COLUMNS_SET = EMBEDDINGS_COLUMNS_SET | set( - METADATA_SELECT_FILTER_COLUMNS -) +EMBEDDINGS_DEFAULT_COLUMNS_SET = set(TIMDEXEmbeddings.DEFAULT_READ_COLUMNS) def test_dataset_embedding_init(): @@ -83,7 +75,8 @@ def test_embeddings_data_root_property(timdex_dataset_empty): timdex_embeddings = TIMDEXEmbeddings(timdex_dataset_empty) expected = f"{timdex_dataset_empty.location.removesuffix('/')}/data/embeddings" - assert timdex_embeddings.data_embeddings_root == expected + assert timdex_embeddings.data_root == expected + assert os.path.exists(expected) def test_embeddings_write_basic(timdex_dataset_empty, sample_embeddings_generator): @@ -95,7 +88,7 @@ def test_embeddings_write_basic(timdex_dataset_empty, sample_embeddings_generato # verify written data can be read dataset = ds.dataset( - timdex_embeddings.data_embeddings_root, format="parquet", partitioning="hive" + timdex_embeddings.data_root, format="parquet", partitioning="hive" ) assert dataset.count_rows() == 100 @@ -116,45 +109,32 @@ def test_embeddings_write_schema_applied( # manually load dataset to confirm schema dataset = ds.dataset( - timdex_embeddings.data_embeddings_root, + timdex_embeddings.data_root, format="parquet", partitioning="hive", ) - assert set(dataset.schema.names) == set(TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names) - - -def test_embeddings_create_batches(timdex_dataset_empty, sample_embeddings_generator): - timdex_embeddings = TIMDEXEmbeddings(timdex_dataset_empty) - total_embeddings = 101 - timdex_dataset_empty.config.write_batch_size = 50 - - batches = list( - timdex_embeddings.create_embedding_batches( - sample_embeddings_generator(total_embeddings) - ) - ) - - assert len(batches) == math.ceil( - total_embeddings / timdex_dataset_empty.config.write_batch_size - ) + assert set(dataset.schema.names) == set(TIMDEXEmbeddings.SCHEMA.names) def test_embeddings_read_batches_yields_pyarrow_record_batches( timdex_dataset_empty, sample_embeddings_generator, sample_records_generator ): # write matching records and rebuild metadata - timdex_dataset_empty.write( + timdex_dataset_empty.records.write( sample_records_generator(100, source="alma", run_id="test-run"), write_append_deltas=False, ) timdex_dataset_empty.metadata.rebuild_dataset_metadata() timdex_dataset_empty.refresh() - # write embeddings and refresh to pick up new views + # write embeddings timdex_dataset_empty.embeddings.write( sample_embeddings_generator(100, run_id="test-run") ) + + # rebuild metadata to include embeddings, then refresh + timdex_dataset_empty.metadata.rebuild_dataset_metadata() timdex_dataset_empty.refresh() batches = timdex_dataset_empty.embeddings.read_batches_iter() @@ -165,7 +145,7 @@ def test_embeddings_read_batches_yields_pyarrow_record_batches( def test_embeddings_read_batches_all_columns_by_default(timdex_embeddings_with_runs): batches = timdex_embeddings_with_runs.read_batches_iter() batch = next(batches) - assert set(batch.column_names) == EMBEDDINGS_WITH_METADATA_COLUMNS_SET + assert set(batch.column_names) == EMBEDDINGS_DEFAULT_COLUMNS_SET def test_embeddings_read_batches_filter_columns(timdex_embeddings_with_runs): @@ -217,7 +197,7 @@ def test_embeddings_read_batches_gets_full_dataset(timdex_embeddings_with_runs): batches = timdex_embeddings_with_runs.read_batches_iter() table = pa.Table.from_batches(batches) dataset = ds.dataset( - timdex_embeddings_with_runs.data_embeddings_root, + timdex_embeddings_with_runs.data_root, format="parquet", partitioning="hive", ) @@ -232,7 +212,7 @@ def test_embeddings_read_batches_with_filters_gets_subset_of_dataset( ) table = pa.Table.from_batches(batches) dataset = ds.dataset( - timdex_embeddings_with_runs.data_embeddings_root, + timdex_embeddings_with_runs.data_root, format="parquet", partitioning="hive", ) @@ -279,7 +259,7 @@ def test_embeddings_read_dataframes_yields_dataframes(timdex_embeddings_with_run def test_embeddings_read_dataframe_gets_full_dataset(timdex_embeddings_with_runs): df = timdex_embeddings_with_runs.read_dataframe() dataset = ds.dataset( - timdex_embeddings_with_runs.data_embeddings_root, + timdex_embeddings_with_runs.data_root, format="parquet", partitioning="hive", ) @@ -293,7 +273,7 @@ def test_embeddings_read_dicts_yields_dictionary_for_each_embeddings_record( dict_iter = timdex_embeddings_with_runs.read_dicts_iter() record = next(dict_iter) assert isinstance(record, dict) - assert set(record.keys()) == EMBEDDINGS_WITH_METADATA_COLUMNS_SET + assert set(record.keys()) == EMBEDDINGS_DEFAULT_COLUMNS_SET def test_current_embeddings_view_single_run(timdex_dataset_for_embeddings_views): @@ -301,6 +281,9 @@ def test_current_embeddings_view_single_run(timdex_dataset_for_embeddings_views) # write embeddings for run "apple-1" td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="apple-1")) + + # rebuild metadata to include embeddings, then refresh + td.metadata.rebuild_dataset_metadata() td.refresh() # query current_embeddings for apple source using read_dataframe @@ -308,7 +291,7 @@ def test_current_embeddings_view_single_run(timdex_dataset_for_embeddings_views) assert len(result) == 10 assert (result["run_id"] == "apple-1").all() - assert (result["run_date"] == date(2025, 6, 1)).all() + assert (result["run_date"] == pd.Timestamp("2025-06-01")).all() def test_current_embeddings_view_multiple_runs(timdex_dataset_for_embeddings_views): @@ -317,6 +300,9 @@ def test_current_embeddings_view_multiple_runs(timdex_dataset_for_embeddings_vie # write embeddings for runs "orange-1" and "orange-2" td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="orange-1")) td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="orange-2")) + + # rebuild metadata to include embeddings, then refresh + td.metadata.rebuild_dataset_metadata() td.refresh() # query current_embeddings for orange source using read_dataframe @@ -329,18 +315,190 @@ def test_current_embeddings_view_multiple_runs(timdex_dataset_for_embeddings_vie # verify 5 from orange-1 (records not in orange-2, run_date 2025-07-01) orange_1_records = result[result["run_id"] == "orange-1"] assert len(orange_1_records) == 5 - assert (orange_1_records["run_date"] == date(2025, 7, 1)).all() + assert (orange_1_records["run_date"] == pd.Timestamp("2025-07-01")).all() # verify 5 from orange-2 (newer records, run_date 2025-07-02) orange_2_records = result[result["run_id"] == "orange-2"] assert len(orange_2_records) == 5 - assert (orange_2_records["run_date"] == date(2025, 7, 2)).all() + assert (orange_2_records["run_date"] == pd.Timestamp("2025-07-02")).all() + + +def test_current_embeddings_prefers_record_recency_over_embedding_recency( + tmp_path, +): + td = TIMDEXDataset(str(tmp_path / "record_recency_wins_dataset/")) + + td.records.write( + generate_sample_records( + num_records=10, + source="pear", + run_date="2025-09-01", + run_type="full", + run_id="pear-1", + ), + write_append_deltas=False, + ) + td.records.write( + generate_sample_records( + num_records=5, + source="pear", + run_date="2025-09-02", + run_type="daily", + run_id="pear-2", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td.refresh() + + # older record version gets a later embedding event + td.embeddings.write( + generate_sample_embeddings_for_run( + td, + run_id="pear-1", + embedding_timestamp="2025-09-04T00:00:00+00:00", + ), + write_append_deltas=False, + ) + td.embeddings.write( + generate_sample_embeddings_for_run( + td, + run_id="pear-2", + embedding_timestamp="2025-09-03T00:00:00+00:00", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td.refresh() + + result = td.embeddings.read_dataframe(table="current_embeddings", source="pear") + + assert len(result) == 10 + + overlap = result[result["timdex_record_id"].isin([f"pear:{i}" for i in range(5)])] + non_overlap = result[ + result["timdex_record_id"].isin([f"pear:{i}" for i in range(5, 10)]) + ] + + assert (overlap["run_id"] == "pear-2").all() + assert (non_overlap["run_id"] == "pear-1").all() + assert ( + overlap["embedding_timestamp"] == pd.Timestamp("2025-09-03T00:00:00+00:00") + ).all() + + +def test_current_embeddings_excludes_superseded_record_versions(tmp_path): + td = TIMDEXDataset(str(tmp_path / "current_embeddings_current_records_only/")) + + td.records.write( + generate_sample_records( + num_records=10, + source="grape", + run_date="2025-09-01", + run_type="full", + run_id="grape-1", + ), + write_append_deltas=False, + ) + td.records.write( + generate_sample_records( + num_records=5, + source="grape", + run_date="2025-09-02", + run_type="daily", + run_id="grape-2", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="grape-1"), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td.refresh() + + result = td.embeddings.read_dataframe(table="current_embeddings", source="grape") + + assert len(result) == 5 + assert set(result["timdex_record_id"]) == {f"grape:{i}" for i in range(5, 10)} + assert (result["run_id"] == "grape-1").all() + + +def test_current_embeddings_view_keeps_models_separate(tmp_path): + td = TIMDEXDataset(str(tmp_path / "multiple_models_current_embeddings/")) + + td.records.write( + generate_sample_records( + num_records=10, + source="plum", + run_date="2025-10-01", + run_type="full", + run_id="plum-1", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td.refresh() + + td.embeddings.write( + generate_sample_embeddings_for_run( + td, + run_id="plum-1", + embedding_model="model-a", + embedding_strategy="full_record", + ), + write_append_deltas=False, + ) + td.embeddings.write( + generate_sample_embeddings_for_run( + td, + run_id="plum-1", + embedding_model="model-b", + embedding_strategy="full_record", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td.refresh() + + result = td.embeddings.read_dataframe(table="current_embeddings", source="plum") + + assert len(result) == 20 + assert set(result["embedding_model"].unique()) == {"model-a", "model-b"} + assert result.groupby("timdex_record_id")["embedding_model"].nunique().eq(2).all() def test_current_embeddings_view_handles_duplicate_run_embeddings( - timdex_dataset_for_embeddings_views, + tmp_path, ): - td = timdex_dataset_for_embeddings_views + """Test that duplicate embeddings for the same run are handled correctly.""" + td = TIMDEXDataset(str(tmp_path / "dup_run_dataset/")) + + # scenario: lemon - full run + daily run (daily will be embedded twice) + td.records.write( + generate_sample_records( + num_records=10, + source="lemon", + run_date="2025-08-01", + run_type="full", + run_id="lemon-1", + ), + write_append_deltas=False, + ) + td.records.write( + generate_sample_records( + num_records=5, + source="lemon", + run_date="2025-08-02", + run_type="daily", + run_id="lemon-2", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td = TIMDEXDataset(td.location) # write embeddings for run "lemon-1" td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="lemon-1")) @@ -358,6 +516,9 @@ def test_current_embeddings_view_handles_duplicate_run_embeddings( td, run_id="lemon-2", embedding_timestamp="2025-08-03T00:00:00+00:00" ) ) + + # rebuild metadata to include embeddings, then refresh + td.metadata.rebuild_dataset_metadata() td.refresh() # check all embeddings for lemon-2 to verify both writes exist @@ -378,20 +539,45 @@ def test_current_embeddings_view_handles_duplicate_run_embeddings( # verify lemon-1 embeddings (run_date 2025-08-01) lemon_1_result = result[result["run_id"] == "lemon-1"] assert len(lemon_1_result) == 5 - assert (lemon_1_result["run_date"] == date(2025, 8, 1)).all() + assert (lemon_1_result["run_date"] == pd.Timestamp("2025-08-01")).all() # verify lemon-2 embeddings have the later embedding timestamp (run_date 2025-08-02) lemon_2_result = result[result["run_id"] == "lemon-2"] assert len(lemon_2_result) == 5 - assert (lemon_2_result["run_date"] == date(2025, 8, 2)).all() + assert (lemon_2_result["run_date"] == pd.Timestamp("2025-08-02")).all() # all lemon-2 current embeddings should have the later embedding timestamp max_timestamp = all_lemon_2["embedding_timestamp"].max() assert (lemon_2_result["embedding_timestamp"] == max_timestamp).all() -def test_embeddings_view_includes_all_embeddings(timdex_dataset_for_embeddings_views): - td = timdex_dataset_for_embeddings_views +def test_embeddings_view_includes_all_embeddings(tmp_path): + """Test that the embeddings view includes all embeddings from multiple writes.""" + td = TIMDEXDataset(str(tmp_path / "all_embeddings_dataset/")) + + # scenario: lemon - full run + daily run (daily will be embedded twice) + td.records.write( + generate_sample_records( + num_records=10, + source="lemon", + run_date="2025-08-01", + run_type="full", + run_id="lemon-1", + ), + write_append_deltas=False, + ) + td.records.write( + generate_sample_records( + num_records=5, + source="lemon", + run_date="2025-08-02", + run_type="daily", + run_id="lemon-2", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td = TIMDEXDataset(td.location) # write embeddings for lemon-1 td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="lemon-1")) @@ -409,6 +595,9 @@ def test_embeddings_view_includes_all_embeddings(timdex_dataset_for_embeddings_v td, run_id="lemon-2", embedding_timestamp="2025-08-03T00:00:00+00:00" ) ) + + # rebuild metadata to include embeddings, then refresh + td.metadata.rebuild_dataset_metadata() td.refresh() # query all embeddings for lemon source @@ -421,22 +610,21 @@ def test_embeddings_view_includes_all_embeddings(timdex_dataset_for_embeddings_v # verify run_date distribution lemon_1_embeddings = result[result["run_id"] == "lemon-1"] assert len(lemon_1_embeddings) == 10 - assert (lemon_1_embeddings["run_date"] == date(2025, 8, 1)).all() + assert (lemon_1_embeddings["run_date"] == pd.Timestamp("2025-08-01")).all() lemon_2_embeddings = result[result["run_id"] == "lemon-2"] assert len(lemon_2_embeddings) == 10 # 5 from each write - assert (lemon_2_embeddings["run_date"] == date(2025, 8, 2)).all() + assert (lemon_2_embeddings["run_date"] == pd.Timestamp("2025-08-02")).all() def test_embeddings_read_batches_iter_returns_empty_when_embeddings_missing( timdex_dataset_empty, caplog ): - result = list(timdex_dataset_empty.embeddings.read_batches_iter()) - assert result == [] - assert ( - "Table 'embeddings' not found in DuckDB context. Embeddings may not yet exist " - "or TIMDEXDataset.refresh() may be required." in caplog.text - ) + with pytest.raises( + ValueError, + match=r"Table 'embeddings' not found in DuckDB context.*rebuild_dataset_metadata", + ): + list(timdex_dataset_empty.embeddings.read_batches_iter()) def test_embeddings_read_batches_iter_returns_empty_for_invalid_table( diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 74cd3fd..6a2e4e0 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -6,19 +6,11 @@ from duckdb import DuckDBPyConnection +from tests.utils import generate_sample_embeddings_for_run, generate_sample_records from timdex_dataset_api import TIMDEXDataset - -ORDERED_METADATA_COLUMN_NAMES = [ - "timdex_record_id", - "source", - "run_date", - "run_type", - "action", - "run_id", - "run_record_offset", - "run_timestamp", - "filename", -] +from timdex_dataset_api.embeddings import TIMDEXEmbeddings +from timdex_dataset_api.metadata import DataTypeMetadataConfig, TIMDEXDatasetMetadata +from timdex_dataset_api.records import TIMDEXRecords def test_tdm_init_no_metadata_file_warning_success(caplog, tmp_path): @@ -31,14 +23,56 @@ def test_tdm_init_no_metadata_file_warning_success(caplog, tmp_path): def test_tdm_local_dataset_structure_properties(tmp_path): local_root = str(Path(tmp_path) / "path/to/nothing") td_local = TIMDEXDataset(local_root) - assert td_local.metadata.location == local_root - assert td_local.metadata.location_scheme == "file" + assert td_local.location == local_root + assert td_local.location_scheme == "file" def test_tdm_s3_dataset_structure_properties(timdex_dataset_empty): # test that location_scheme property works correctly for local paths # S3 tests require full mocking and are covered in other tests - assert timdex_dataset_empty.metadata.location_scheme == "file" + assert timdex_dataset_empty.location_scheme == "file" + + +def test_data_type_metadata_config_prejoin_records_default_true(): + config = DataTypeMetadataConfig( + name="example", + metadata_columns=["timdex_record_id"], + data_path="data/example", + ) + assert config.prejoin_records is True + + +def test_data_source_metadata_configs_are_derived_from_base_class(): + assert TIMDEXRecords.METADATA_CONFIG.name == TIMDEXRecords.NAME + assert TIMDEXRecords.METADATA_CONFIG.data_path == TIMDEXRecords.DATA_PATH + assert TIMDEXRecords.METADATA_CONFIG.prejoin_records is False + assert ( + TIMDEXRecords.METADATA_CONFIG.metadata_columns + == TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS + ) + + assert TIMDEXEmbeddings.METADATA_CONFIG.name == TIMDEXEmbeddings.NAME + assert TIMDEXEmbeddings.METADATA_CONFIG.data_path == TIMDEXEmbeddings.DATA_PATH + assert TIMDEXEmbeddings.METADATA_CONFIG.prejoin_records is True + assert TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns == [ + "timdex_record_id", + "run_id", + "run_record_offset", + *TIMDEXEmbeddings.ADDITIONAL_METADATA_COLUMNS, + "filename", + ] + + +def test_dataset_registers_current_view_specs_from_data_sources(tmp_path): + td = TIMDEXDataset(str(tmp_path / "register_current_view_specs")) + + expected_view_names = [ + spec.name + for spec in ( + TIMDEXRecords.CURRENT_VIEW_SPECS + TIMDEXEmbeddings.CURRENT_VIEW_SPECS + ) + ] + assert [spec.name for spec in td.current_metadata_view_specs] == expected_view_names def test_tdm_create_metadata_database_file_success( @@ -51,12 +85,12 @@ def test_tdm_create_metadata_database_file_success( def test_tdm_init_metadata_file_found_success(timdex_metadata): - assert isinstance(timdex_metadata.conn, DuckDBPyConnection) + assert isinstance(timdex_metadata.timdex_dataset.conn, DuckDBPyConnection) def test_tdm_duckdb_context_creates_metadata_schema(timdex_metadata): assert ( - timdex_metadata.conn.query(""" + timdex_metadata.timdex_dataset.conn.query(""" select count(*) from information_schema.schemata where catalog_name = 'memory' @@ -68,12 +102,14 @@ def test_tdm_duckdb_context_creates_metadata_schema(timdex_metadata): def test_tdm_connection_has_static_database_attached(timdex_metadata): assert set( - timdex_metadata.conn.query("""show databases;""").to_df().database_name + timdex_metadata.timdex_dataset.conn.query("""show databases;""") + .to_df() + .database_name ) == {"memory", "static_db"} def test_tdm_connection_static_database_records_table_exists(timdex_metadata): - records_df = timdex_metadata.conn.query( + records_df = timdex_metadata.timdex_dataset.conn.query( """select * from static_db.records;""" ).to_df() assert len(records_df) > 0 @@ -91,17 +127,68 @@ def test_dataset_metadata_structure_is_idempotent(timdex_metadata): def test_tdm_views_created_on_init(timdex_metadata): - views = timdex_metadata.conn.query( + views = timdex_metadata.timdex_dataset.conn.query( """select table_name from information_schema.tables where table_type = 'VIEW';""" ).to_df() - expected_views = {"append_deltas", "records", "current_records"} + expected_views = {"records_append_deltas", "records", "current_records"} actual_views = set(views.table_name) assert expected_views <= actual_views +def test_tdm_current_view_specs_missing_dependencies_are_skipped_generically( + caplog, tmp_path +): + dataset_path = str(tmp_path / "current_view_missing_dependencies") + + td = TIMDEXDataset(dataset_path) + td.records.write( + generate_sample_records( + num_records=10, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="missing-deps-run", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + caplog.set_level("WARNING") + caplog.clear() + + td_with_metadata = TIMDEXDataset(dataset_path) + + metadata_objects = td_with_metadata.conn.query(""" + select table_name + from information_schema.tables + where table_schema = 'metadata' + """).to_df() + metadata_names = set(metadata_objects.table_name) + + missing_specs = [] + for spec in td_with_metadata.current_metadata_view_specs: + missing_required_tables = [ + table_name + for table_name in spec.required_metadata_tables + if table_name not in metadata_names + ] + if not missing_required_tables: + continue + + missing_specs.append(spec.name) + assert spec.name not in metadata_names + assert ( + "Skipping metadata." + f"{spec.name} view creation because missing dependencies: " + f"{', '.join(missing_required_tables)}" + ) in caplog.text + + assert missing_specs + + def test_tdm_records_view_structure(timdex_metadata): - records_df = timdex_metadata.conn.query( + records_df = timdex_metadata.timdex_dataset.conn.query( """select * from metadata.records limit 1;""" ).to_df() expected_columns = { @@ -119,7 +206,7 @@ def test_tdm_records_view_structure(timdex_metadata): def test_tdm_current_records_view_structure(timdex_metadata): - current_records_df = timdex_metadata.conn.query( + current_records_df = timdex_metadata.timdex_dataset.conn.query( """select * from metadata.current_records limit 1;""" ).to_df() expected_columns = { @@ -137,8 +224,8 @@ def test_tdm_current_records_view_structure(timdex_metadata): def test_tdm_append_deltas_view_empty_structure(timdex_metadata): - append_deltas_df = timdex_metadata.conn.query( - """select * from metadata.append_deltas;""" + append_deltas_df = timdex_metadata.timdex_dataset.conn.query( + """select * from metadata.records_append_deltas;""" ).to_df() expected_columns = { "timdex_record_id", @@ -150,6 +237,7 @@ def test_tdm_append_deltas_view_empty_structure(timdex_metadata): "run_record_offset", "run_timestamp", "filename", + "append_delta_filename", } assert set(append_deltas_df.columns) == expected_columns assert len(append_deltas_df) == 0 @@ -158,7 +246,7 @@ def test_tdm_append_deltas_view_empty_structure(timdex_metadata): def test_tdm_records_count_property(timdex_metadata): assert timdex_metadata.records_count > 0 - manual_count = timdex_metadata.conn.query( + manual_count = timdex_metadata.timdex_dataset.conn.query( """select count(*) from metadata.records;""" ).fetchone()[0] assert timdex_metadata.records_count == manual_count @@ -167,7 +255,7 @@ def test_tdm_records_count_property(timdex_metadata): def test_tdm_current_records_count_property(timdex_metadata): assert timdex_metadata.current_records_count > 0 - manual_count = timdex_metadata.conn.query( + manual_count = timdex_metadata.timdex_dataset.conn.query( """select count(*) from metadata.current_records;""" ).fetchone()[0] assert timdex_metadata.current_records_count == manual_count @@ -178,10 +266,10 @@ def test_tdm_append_deltas_count_property_empty(timdex_metadata): def test_tdm_records_equals_static_without_deltas(timdex_metadata): - static_count = timdex_metadata.conn.query( + static_count = timdex_metadata.timdex_dataset.conn.query( """select count(*) from static_db.records;""" ).fetchone()[0] - records_count = timdex_metadata.conn.query( + records_count = timdex_metadata.timdex_dataset.conn.query( """select count(*) from metadata.records;""" ).fetchone()[0] assert static_count == records_count @@ -196,11 +284,11 @@ def test_tdm_current_records_filtering_logic(timdex_metadata): def test_tdm_views_with_append_deltas(timdex_metadata_with_deltas): - views = timdex_metadata_with_deltas.conn.query( + views = timdex_metadata_with_deltas.timdex_dataset.conn.query( """select table_name from information_schema.tables where table_type = 'VIEW';""" ).to_df() - expected_views = {"append_deltas", "records", "current_records"} + expected_views = {"records_append_deltas", "records", "current_records"} actual_views = set(views.table_name) assert expected_views.issubset(actual_views) @@ -211,7 +299,7 @@ def test_tdm_append_deltas_view_has_data(timdex_metadata_with_deltas): def test_tdm_records_includes_deltas(timdex_metadata_with_deltas): - static_count = timdex_metadata_with_deltas.conn.query( + static_count = timdex_metadata_with_deltas.timdex_dataset.conn.query( """select count(*) from static_db.records;""" ).fetchone()[0] deltas_count = timdex_metadata_with_deltas.append_deltas_count @@ -229,7 +317,7 @@ def test_tdm_current_records_with_deltas_logic(timdex_metadata_with_deltas): assert current_count > 0 # verify current records view returns unique timdex_record_id values - current_records_df = timdex_metadata_with_deltas.conn.query( + current_records_df = timdex_metadata_with_deltas.timdex_dataset.conn.query( """select timdex_record_id from metadata.current_records;""" ).to_df() @@ -239,7 +327,7 @@ def test_tdm_current_records_with_deltas_logic(timdex_metadata_with_deltas): def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas): # check that for records with multiple versions, only the most recent is returned - multi_version_records = timdex_metadata_with_deltas.conn.query(""" + multi_version_records = timdex_metadata_with_deltas.timdex_dataset.conn.query(""" select timdex_record_id, count(*) as version_count from metadata.records group by timdex_record_id @@ -251,7 +339,7 @@ def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas): record_id = multi_version_records.iloc[0]["timdex_record_id"] # get most recent timestamp for this record - most_recent = timdex_metadata_with_deltas.conn.query(f""" + most_recent = timdex_metadata_with_deltas.timdex_dataset.conn.query(f""" select run_timestamp, run_id from metadata.records where timdex_record_id = '{record_id}' @@ -260,7 +348,7 @@ def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas): """).to_df() # verify current_records contains this version - current_version = timdex_metadata_with_deltas.conn.query(f""" + current_version = timdex_metadata_with_deltas.timdex_dataset.conn.query(f""" select run_timestamp, run_id from metadata.current_records where timdex_record_id = '{record_id}'; @@ -277,7 +365,7 @@ def test_tdm_current_records_most_recent_version(timdex_metadata_with_deltas): def test_tdm_merge_append_deltas_static_counts_match_records_count_before_merge( timdex_metadata_with_deltas, timdex_metadata_merged_deltas ): - static_count_merged_deltas = timdex_metadata_merged_deltas.conn.query( + static_count_merged_deltas = timdex_metadata_merged_deltas.timdex_dataset.conn.query( """select count(*) as count from static_db.records;""" ).fetchone()[0] assert static_count_merged_deltas == timdex_metadata_with_deltas.records_count @@ -286,15 +374,16 @@ def test_tdm_merge_append_deltas_static_counts_match_records_count_before_merge( def test_tdm_merge_append_deltas_adds_records_to_static_db( timdex_metadata_with_deltas, timdex_metadata_merged_deltas ): - append_deltas = timdex_metadata_with_deltas.conn.query(f""" + columns = ",".join(TIMDEXRecords.METADATA_CONFIG.metadata_columns) + append_deltas = timdex_metadata_with_deltas.timdex_dataset.conn.query(f""" select - {",".join(ORDERED_METADATA_COLUMN_NAMES)} - from metadata.append_deltas + {columns} + from metadata.records_append_deltas """).to_df() - merged_static_db = timdex_metadata_merged_deltas.conn.query(f""" + merged_static_db = timdex_metadata_merged_deltas.timdex_dataset.conn.query(f""" select - {",".join(ORDERED_METADATA_COLUMN_NAMES)} + {columns} from static_db.records """).to_df() @@ -306,11 +395,463 @@ def test_tdm_merge_append_deltas_adds_records_to_static_db( def test_tdm_merge_append_deltas_deletes_append_deltas( timdex_metadata_with_deltas, timdex_metadata_merged_deltas ): + records_deltas_path_before = timdex_metadata_with_deltas.append_deltas_path_for( + TIMDEXRecords.METADATA_CONFIG + ) + records_deltas_path_after = timdex_metadata_merged_deltas.append_deltas_path_for( + TIMDEXRecords.METADATA_CONFIG + ) + assert timdex_metadata_with_deltas.append_deltas_count != 0 - assert os.listdir(timdex_metadata_with_deltas.append_deltas_path) + assert os.listdir(records_deltas_path_before) assert timdex_metadata_merged_deltas.append_deltas_count == 0 - assert not os.listdir(timdex_metadata_merged_deltas.append_deltas_path) + assert not os.listdir(records_deltas_path_after) + + +def test_tdm_embeddings_metadata_view_structure(tmp_path): + td = TIMDEXDataset(str(tmp_path / "embeddings_metadata_structure")) + + td.records.write( + generate_sample_records( + num_records=25, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="emb-structure-run", + ), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="emb-structure-run"), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + embeddings_df = td.conn.query( + """select * from metadata.embeddings limit 1;""" + ).to_df() + assert len(embeddings_df) == 1 + # pre-joined view includes native embeddings columns + records columns + expected_columns = set(TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns) | { + "source", + "run_date", + "run_type", + "action", + "run_timestamp", + } + assert set(embeddings_df.columns) == expected_columns + + +def test_tdm_current_embeddings_view_structure(tmp_path): + td = TIMDEXDataset(str(tmp_path / "current_embeddings_structure")) + + td.records.write( + generate_sample_records( + num_records=25, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="emb-current-structure-run", + ), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="emb-current-structure-run"), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + current_embeddings_df = td.conn.query( + """select * from metadata.current_embeddings limit 1;""" + ).to_df() + + assert len(current_embeddings_df) == 1 + # pre-joined view includes native embeddings columns + records columns + expected_columns = set(TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns) | { + "source", + "run_date", + "run_type", + "action", + "run_timestamp", + } + assert set(current_embeddings_df.columns) == expected_columns + + +def test_tdm_current_embeddings_latest_per_record_strategy(tmp_path): + td = TIMDEXDataset(str(tmp_path / "current_embeddings_latest")) + + td.records.write( + generate_sample_records( + num_records=10, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="emb-current-latest-run-1", + ), + write_append_deltas=False, + ) + td.records.write( + generate_sample_records( + num_records=5, + source="alma", + run_date="2025-03-02", + run_type="daily", + run_id="emb-current-latest-run-2", + ), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run( + td, + run_id="emb-current-latest-run-1", + embedding_timestamp="2025-03-10T00:00:00+00:00", + ), + write_append_deltas=False, + ) + td.embeddings.write( + generate_sample_embeddings_for_run( + td, + run_id="emb-current-latest-run-2", + embedding_timestamp="2025-03-11T00:00:00+00:00", + ), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + current_embeddings_df = td.conn.query(""" + select + timdex_record_id, + run_id, + embedding_strategy + from metadata.current_embeddings + """).to_df() + + expected_total_rows = 10 + expected_run_1_rows = 5 + expected_run_2_rows = 5 + + assert len(current_embeddings_df) == expected_total_rows + assert ( + len( + current_embeddings_df[ + current_embeddings_df.run_id == "emb-current-latest-run-1" + ] + ) + == expected_run_1_rows + ) + assert ( + len( + current_embeddings_df[ + current_embeddings_df.run_id == "emb-current-latest-run-2" + ] + ) + == expected_run_2_rows + ) + + +def test_tdm_current_run_embeddings_view_structure(tmp_path): + td = TIMDEXDataset(str(tmp_path / "current_run_embeddings_structure")) + + td.records.write( + generate_sample_records( + num_records=25, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="emb-current-run-structure-run", + ), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="emb-current-run-structure-run"), + write_append_deltas=False, + ) + + td.metadata.rebuild_dataset_metadata() + + current_run_embeddings_df = td.conn.query( + """select * from metadata.current_run_embeddings limit 1;""" + ).to_df() + + assert len(current_run_embeddings_df) == 1 + # pre-joined view includes native embeddings columns + records columns + expected_columns = set(TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns) | { + "source", + "run_date", + "run_type", + "action", + "run_timestamp", + } + assert set(current_run_embeddings_df.columns) == expected_columns + + +def test_tdm_prejoined_embeddings_view_has_correct_source_values(tmp_path): + """Verify pre-joined source column matches the underlying records.""" + td = TIMDEXDataset(str(tmp_path / "prejoin_source_values")) + + td.records.write( + generate_sample_records( + num_records=10, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="prejoin-run-1", + ), + write_append_deltas=False, + ) + td.records.write( + generate_sample_records( + num_records=10, + source="dspace", + run_date="2025-03-02", + run_type="full", + run_id="prejoin-run-2", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="prejoin-run-1"), + write_append_deltas=False, + ) + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="prejoin-run-2"), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + # all embeddings from run-1 should have source='alma' + alma_embeddings = td.conn.query(""" + select count(*) from metadata.embeddings + where run_id = 'prejoin-run-1' and source = 'alma' + """).fetchone()[0] + assert alma_embeddings == 10 # noqa: PLR2004 + + # all embeddings from run-2 should have source='dspace' + dspace_embeddings = td.conn.query(""" + select count(*) from metadata.embeddings + where run_id = 'prejoin-run-2' and source = 'dspace' + """).fetchone()[0] + assert dspace_embeddings == 10 # noqa: PLR2004 + + # verify current_embeddings also has pre-joined source column + alma_current = td.conn.query(""" + select count(*) from metadata.current_embeddings + where source = 'alma' + """).fetchone()[0] + dspace_current = td.conn.query(""" + select count(*) from metadata.current_embeddings + where source = 'dspace' + """).fetchone()[0] + assert alma_current == 10 # noqa: PLR2004 + assert dspace_current == 10 # noqa: PLR2004 + + +def test_tdm_prejoined_embeddings_filterable_by_run_date(tmp_path): + """Verify pre-joined run_date column is usable for filtering.""" + td = TIMDEXDataset(str(tmp_path / "prejoin_run_date_filter")) + + td.records.write( + generate_sample_records( + num_records=10, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="filter-run-1", + ), + write_append_deltas=False, + ) + td.records.write( + generate_sample_records( + num_records=10, + source="alma", + run_date="2025-04-01", + run_type="full", + run_id="filter-run-2", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="filter-run-1"), + write_append_deltas=False, + ) + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="filter-run-2"), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + # filter embeddings by run_date + march_embeddings = td.conn.query(""" + select count(*) from metadata.embeddings + where run_date = cast('2025-03-01' as date) + """).fetchone()[0] + assert march_embeddings == 10 # noqa: PLR2004 + + april_embeddings = td.conn.query(""" + select count(*) from metadata.embeddings + where run_date = cast('2025-04-01' as date) + """).fetchone()[0] + assert april_embeddings == 10 # noqa: PLR2004 + + +def test_tdm_keyset_paginated_query_on_prejoined_embeddings_view(tmp_path): + """Verify build_keyset_paginated_metadata_query works on pre-joined embeddings.""" + td = TIMDEXDataset(str(tmp_path / "keyset_prejoin_embeddings")) + + td.records.write( + generate_sample_records( + num_records=25, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="keyset-prejoin-run", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="keyset-prejoin-run"), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + td.reflect_sa_tables() + + # build a keyset pagination query against the pre-joined embeddings view + query = td.metadata.build_keyset_paginated_metadata_query( + "embeddings", + limit=10, + keyset_value=(0, 0, 0), + ) + + # execute and verify results + result_df = td.conn.query(query).to_df() + assert len(result_df) == 10 # noqa: PLR2004 + expected_cols = set( + TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS + + TIMDEXEmbeddings.ADDITIONAL_METADATA_COLUMNS + + ["run_id_hash", "filename_hash"] + ) + assert set(result_df.columns) == expected_cols + + +def test_tdm_embeddings_write_append_deltas_without_static_embeddings_table(tmp_path): + record_count = 20 + td = TIMDEXDataset(str(tmp_path / "embeddings_append_deltas_only")) + + # build records metadata only + td.records.write( + generate_sample_records( + num_records=record_count, + source="alma", + run_date="2025-03-02", + run_type="full", + run_id="emb-delta-run", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + # write embeddings with append deltas (without rebuilding static metadata first) + td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="emb-delta-run")) + + # embeddings metadata views should still exist and include append deltas + embeddings_count = td.conn.query( + """select count(*) from metadata.embeddings;""" + ).fetchone()[0] + embeddings_deltas_count = td.conn.query( + """select count(*) from metadata.embeddings_append_deltas;""" + ).fetchone()[0] + + embeddings_deltas_path = td.metadata.append_deltas_path_for( + TIMDEXEmbeddings.METADATA_CONFIG + ) + assert embeddings_count == record_count + assert embeddings_deltas_count == record_count + assert os.listdir(embeddings_deltas_path) + + +def test_tdm_merge_append_deltas_merges_embeddings(tmp_path): + run_1_count = 30 + run_2_count = 10 + td = TIMDEXDataset(str(tmp_path / "embeddings_merge")) + + # write records + initial embeddings and rebuild so static_db.embeddings exists + td.records.write( + generate_sample_records( + num_records=run_1_count, + source="alma", + run_date="2025-03-03", + run_type="full", + run_id="emb-merge-run-1", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write( + generate_sample_embeddings_for_run(td, run_id="emb-merge-run-1"), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + # write second embeddings run with append deltas + td.records.write( + generate_sample_records( + num_records=run_2_count, + source="alma", + run_date="2025-03-04", + run_type="daily", + run_id="emb-merge-run-2", + ), + write_append_deltas=False, + ) + td.metadata.rebuild_dataset_metadata() + + td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="emb-merge-run-2")) + + embeddings_count_before_merge = td.conn.query( + """select count(*) from metadata.embeddings;""" + ).fetchone()[0] + assert ( + td.conn.query( + """select count(*) from metadata.embeddings_append_deltas;""" + ).fetchone()[0] + == run_2_count + ) + + td.metadata.merge_append_deltas() + td.refresh() + + embeddings_static_after_merge = td.conn.query( + """select count(*) from static_db.embeddings;""" + ).fetchone()[0] + embeddings_deltas_after_merge = td.conn.query( + """select count(*) from metadata.embeddings_append_deltas;""" + ).fetchone()[0] + + assert embeddings_static_after_merge == embeddings_count_before_merge + assert embeddings_deltas_after_merge == 0 def test_td_prepare_duckdb_secret_and_extensions_home_env_var_set_and_valid( @@ -379,13 +920,13 @@ def test_td_prepare_duckdb_secret_and_extensions_home_env_var_set_but_empty( def test_td_preload_current_records_default_false(tmp_path): td = TIMDEXDataset(str(tmp_path)) assert td.preload_current_records is False - assert td.metadata.preload_current_records is False + assert td.preload_current_records is False def test_td_preload_current_records_flag_true(tmp_path): td = TIMDEXDataset(str(tmp_path), preload_current_records=True) assert td.preload_current_records is True - assert td.metadata.preload_current_records is True + assert td.preload_current_records is True def test_tdm_preload_false_no_temp_table(timdex_dataset_with_runs): @@ -393,7 +934,7 @@ def test_tdm_preload_false_no_temp_table(timdex_dataset_with_runs): td = TIMDEXDataset(timdex_dataset_with_runs.location) # assert that materialized, temporary table "temp.current_records" does not exist - temp_table_count = td.metadata.conn.query(""" + temp_table_count = td.conn.query(""" select count(*) from information_schema.tables where table_catalog = 'temp' @@ -410,7 +951,7 @@ def test_tdm_preload_true_has_temp_table(timdex_dataset_with_runs): td = TIMDEXDataset(timdex_dataset_with_runs.location, preload_current_records=True) # assert that materialized, temporary table "temp.current_records" does exist - temp_table_count = td.metadata.conn.query(""" + temp_table_count = td.conn.query(""" select count(*) from information_schema.tables where table_catalog = 'temp' diff --git a/tests/test_read.py b/tests/test_read.py index 9941522..c3ded3a 100644 --- a/tests/test_read.py +++ b/tests/test_read.py @@ -6,38 +6,50 @@ import pytest from duckdb import ParserException -from timdex_dataset_api.dataset import TIMDEX_DATASET_SCHEMA +from timdex_dataset_api.records import TIMDEXRecords -DATASET_COLUMNS_SET = set(TIMDEX_DATASET_SCHEMA.names) +DATASET_COLUMNS_SET = set(TIMDEXRecords.DEFAULT_READ_COLUMNS) + + +def _count_rows_via_duckdb_parquet(timdex_dataset) -> int: + return timdex_dataset.conn.query(f""" + select count(*) + from read_parquet( + '{timdex_dataset.records.data_root}/**/*.parquet', + hive_partitioning=true + ) + """).fetchone()[0] def test_read_batches_yields_pyarrow_record_batches(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_batches_iter() + batches = timdex_dataset_multi_source.records.read_batches_iter() batch = next(batches) assert isinstance(batch, pa.RecordBatch) def test_read_batches_all_columns_by_default(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_batches_iter() + batches = timdex_dataset_multi_source.records.read_batches_iter() batch = next(batches) assert set(batch.column_names) == DATASET_COLUMNS_SET def test_read_batches_filter_columns(timdex_dataset_multi_source): columns_subset = ["source", "transformed_record"] - batches = timdex_dataset_multi_source.read_batches_iter(columns=columns_subset) + batches = timdex_dataset_multi_source.records.read_batches_iter( + columns=columns_subset + ) batch = next(batches) assert set(batch.column_names) == set(columns_subset) def test_read_batches_no_filters_gets_full_dataset(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_batches_iter() + batches = timdex_dataset_multi_source.records.read_batches_iter() table = pa.Table.from_batches(batches) - assert len(table) == timdex_dataset_multi_source.dataset.count_rows() + assert len(table) == _count_rows_via_duckdb_parquet(timdex_dataset_multi_source) def test_read_batches_with_filters_gets_subset_of_dataset(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_batches_iter( + batches = timdex_dataset_multi_source.records.read_batches_iter( source="libguides", run_date="2024-12-01", run_type="daily", @@ -45,15 +57,17 @@ def test_read_batches_with_filters_gets_subset_of_dataset(timdex_dataset_multi_s ) table = pa.Table.from_batches(batches) + total_rows = _count_rows_via_duckdb_parquet(timdex_dataset_multi_source) + assert len(table) == 1_000 - assert len(table) < timdex_dataset_multi_source.dataset.count_rows() + assert len(table) < total_rows - # assert loaded dataset is unchanged by filtering for a read method - assert timdex_dataset_multi_source.dataset.count_rows() == 5_000 + # assert loaded parquet data is unchanged by filtering for a read method + assert total_rows == 5_000 def test_read_dataframes_yields_dataframes(timdex_dataset_multi_source): - df_iter = timdex_dataset_multi_source.read_dataframes_iter() + df_iter = timdex_dataset_multi_source.records.read_dataframes_iter() df_batch = next(df_iter) assert isinstance(df_batch, pd.DataFrame) assert len(df_batch) == 1_000 @@ -62,47 +76,51 @@ def test_read_dataframes_yields_dataframes(timdex_dataset_multi_source): def test_read_dataframe_gets_full_dataset( timdex_dataset_multi_source, ): - df = timdex_dataset_multi_source.read_dataframe() + df = timdex_dataset_multi_source.records.read_dataframe() assert isinstance(df, pd.DataFrame) - assert len(df) == timdex_dataset_multi_source.dataset.count_rows() + assert len(df) == _count_rows_via_duckdb_parquet(timdex_dataset_multi_source) def test_read_dicts_yields_dictionary_for_each_dataset_record( timdex_dataset_multi_source, ): - records = timdex_dataset_multi_source.read_dicts_iter() + records = timdex_dataset_multi_source.records.read_dicts_iter() record = next(records) assert isinstance(record, dict) assert set(record.keys()) == DATASET_COLUMNS_SET def test_read_batches_filter_to_none_returns_empty_list(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_batches_iter(source="not-gonna-find-me") + batches = timdex_dataset_multi_source.records.read_batches_iter( + source="not-gonna-find-me" + ) assert list(batches) == [] def test_read_dicts_filter_to_none_stopiteration_immediately(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_dicts_iter(source="not-gonna-find-me") + batches = timdex_dataset_multi_source.records.read_dicts_iter( + source="not-gonna-find-me" + ) with pytest.raises(StopIteration): next(batches) def test_read_transformed_records_yields_parsed_dictionary(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_transformed_records_iter() + batches = timdex_dataset_multi_source.records.read_transformed_records_iter() transformed_record = next(batches) assert isinstance(transformed_record, dict) assert transformed_record == {"title": ["Hello World."]} def test_read_batches_where_filters_response(timdex_dataset_multi_source): - df_all = timdex_dataset_multi_source.read_dataframe() + df_all = timdex_dataset_multi_source.records.read_dataframe() total_count = len(df_all) where = ( "source = 'libguides' AND run_date = '2024-12-01' AND " "run_type = 'daily' AND action = 'index'" ) - df_where = timdex_dataset_multi_source.read_dataframe(where=where) + df_where = timdex_dataset_multi_source.records.read_dataframe(where=where) assert len(df_where) == 1_000 assert len(df_where) < total_count @@ -112,7 +130,7 @@ def test_read_batches_where_and_dataset_filters_are_combined(timdex_dataset_mult """Test that when key/value DatasetFilters AND a SQL where clause is provided, they are combined in the final DuckDB SQL query.""" where = "run_date = '2024-12-01' AND run_type = 'daily'" - df = timdex_dataset_multi_source.read_dataframe( + df = timdex_dataset_multi_source.records.read_dataframe( where=where, source="libguides", action="index" ) assert len(df) == 1_000 @@ -133,12 +151,12 @@ def test_read_batches_where_rejects_non_predicate_sql( timdex_dataset_multi_source, bad_where ): with pytest.raises(ParserException): - next(timdex_dataset_multi_source.read_batches_iter(where=bad_where)) + next(timdex_dataset_multi_source.records.read_batches_iter(where=bad_where)) def test_read_dataframe_respects_where(timdex_dataset_multi_source): where = "source = 'libguides' AND action = 'index'" - df = timdex_dataset_multi_source.read_dataframe(where=where) + df = timdex_dataset_multi_source.records.read_dataframe(where=where) assert len(df) > 0 assert set(df["source"].unique().tolist()) == {"libguides"} assert set(df["action"].unique().tolist()) == {"index"} @@ -146,14 +164,16 @@ def test_read_dataframe_respects_where(timdex_dataset_multi_source): def test_read_dicts_iter_respects_where_and_filters(timdex_dataset_multi_source): where = "run_type = 'daily'" - it = timdex_dataset_multi_source.read_dicts_iter(where=where, source="libguides") + it = timdex_dataset_multi_source.records.read_dicts_iter( + where=where, source="libguides" + ) first = next(it) assert first["run_type"] == "daily" assert first["source"] == "libguides" def test_dataset_all_current_records_deduped(timdex_dataset_with_runs_with_metadata): - df = timdex_dataset_with_runs_with_metadata.read_dataframe( + df = timdex_dataset_with_runs_with_metadata.records.read_dataframe( table="current_records", columns=["timdex_record_id"], ) @@ -162,7 +182,7 @@ def test_dataset_all_current_records_deduped(timdex_dataset_with_runs_with_metad def test_dataset_source_current_records_deduped(timdex_dataset_with_runs_with_metadata): - df = timdex_dataset_with_runs_with_metadata.read_dataframe( + df = timdex_dataset_with_runs_with_metadata.records.read_dataframe( table="current_records", source="alma" ) assert df is not None @@ -174,17 +194,17 @@ def test_dataset_all_read_methods_get_deduplication( timdex_dataset_with_runs_with_metadata, ): batch_rows = 0 - for b in timdex_dataset_with_runs_with_metadata.read_batches_iter( + for b in timdex_dataset_with_runs_with_metadata.records.read_batches_iter( table="current_records", columns=["timdex_record_id"] ): batch_rows += len(b) dict_rows = sum( 1 - for _ in timdex_dataset_with_runs_with_metadata.read_dicts_iter( + for _ in timdex_dataset_with_runs_with_metadata.records.read_dicts_iter( table="current_records", columns=["timdex_record_id"] ) ) - df = timdex_dataset_with_runs_with_metadata.read_dataframe( + df = timdex_dataset_with_runs_with_metadata.records.read_dataframe( table="current_records", columns=["timdex_record_id"] ) assert df is not None @@ -195,11 +215,11 @@ def test_dataset_all_read_methods_get_deduplication( def test_dataset_current_records_no_additional_filtering_accurate_records_yielded( timdex_dataset_with_runs_with_metadata, ): - df_all = timdex_dataset_with_runs_with_metadata.read_dataframe( + df_all = timdex_dataset_with_runs_with_metadata.records.read_dataframe( table="current_records" ) assert df_all is not None - df_total = timdex_dataset_with_runs_with_metadata.read_dataframe() + df_total = timdex_dataset_with_runs_with_metadata.records.read_dataframe() assert df_total is not None assert len(df_all) <= len(df_total) assert df_all["timdex_record_id"].nunique() == len(df_all) @@ -208,7 +228,7 @@ def test_dataset_current_records_no_additional_filtering_accurate_records_yielde def test_dataset_current_records_action_filtering_accurate_records_yielded( timdex_dataset_with_runs_with_metadata, ): - df = timdex_dataset_with_runs_with_metadata.read_dataframe( + df = timdex_dataset_with_runs_with_metadata.records.read_dataframe( table="current_records", action="index" ) assert df is not None @@ -219,14 +239,14 @@ def test_dataset_current_records_index_filtering_accurate_records_yielded( timdex_dataset_with_runs_with_metadata, ): # with all records, run-5 has 25 rows - df_all = timdex_dataset_with_runs_with_metadata.read_dataframe( + df_all = timdex_dataset_with_runs_with_metadata.records.read_dataframe( source="alma", run_id="run-5" ) assert df_all is not None assert len(df_all) == 25 # within current_records, only 15 remain due to later deletes - df_current = timdex_dataset_with_runs_with_metadata.read_dataframe( + df_current = timdex_dataset_with_runs_with_metadata.records.read_dataframe( table="current_records", source="alma", run_id="run-5" ) assert df_current is not None @@ -255,7 +275,7 @@ def test_dataset_load_current_records_gets_correct_same_day_full_run( ): # ensure metadata exists for this dataset timdex_dataset_same_day_runs.metadata.rebuild_dataset_metadata() - df = timdex_dataset_same_day_runs.read_dataframe( + df = timdex_dataset_same_day_runs.records.read_dataframe( table="current_records", run_type="full" ) assert list(df.run_id.unique()) == ["run-2"] @@ -266,7 +286,7 @@ def test_dataset_load_current_records_gets_correct_same_day_daily_runs_ordering( ): timdex_dataset_same_day_runs.metadata.rebuild_dataset_metadata() first_record = next( - timdex_dataset_same_day_runs.read_dicts_iter( + timdex_dataset_same_day_runs.records.read_dicts_iter( table="current_records", run_type="daily" ) ) @@ -277,13 +297,13 @@ def test_dataset_load_current_records_gets_correct_same_day_daily_runs_ordering( def test_read_batches_iter_limit_returns_n_rows(timdex_dataset_multi_source): - batches = timdex_dataset_multi_source.read_batches_iter(limit=10) + batches = timdex_dataset_multi_source.records.read_batches_iter(limit=10) table = pa.Table.from_batches(batches) assert len(table) == 10 -def test_read_batches_iter_returns_empty_when_metadata_missing( - timdex_dataset_empty, caplog +def test_read_batches_iter_raises_when_metadata_missing( + timdex_dataset_empty, ): with pytest.raises( ValueError, @@ -293,15 +313,14 @@ def test_read_batches_iter_returns_empty_when_metadata_missing( "TIMDEXDataset.metadata.rebuild_dataset_metadata() may be required." ), ): - list(timdex_dataset_empty.read_batches_iter()) + list(timdex_dataset_empty.records.read_batches_iter()) -def test_read_batches_iter_returns_empty_for_invalid_table( - timdex_dataset_multi_source, caplog +def test_read_batches_iter_raises_for_invalid_table( + timdex_dataset_multi_source, ): - """read_batches_iter returns empty iterator for nonexistent table name.""" with pytest.raises( ValueError, match="Invalid table: 'nonexistent'", ): - list(timdex_dataset_multi_source.read_batches_iter(table="nonexistent")) + list(timdex_dataset_multi_source.records.read_batches_iter(table="nonexistent")) diff --git a/tests/test_records.py b/tests/test_records.py index 3e3d4db..c960cd4 100644 --- a/tests/test_records.py +++ b/tests/test_records.py @@ -3,7 +3,7 @@ import pytest -from timdex_dataset_api.record import DatasetRecord +from timdex_dataset_api.records import DatasetRecord def test_dataset_record_init_with_valid_run_date_parses_year_month_day(): diff --git a/tests/test_write.py b/tests/test_write.py index 3710989..6bc0612 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -1,4 +1,5 @@ # ruff: noqa: PLR2004, D209, D205 +import glob import math import os from pathlib import Path @@ -8,20 +9,47 @@ import pyarrow.parquet as pq from tests.utils import generate_sample_records -from timdex_dataset_api.dataset import ( - TIMDEX_DATASET_SCHEMA, -) -from timdex_dataset_api.metadata import ORDERED_METADATA_COLUMN_NAMES +from timdex_dataset_api.records import TIMDEXRecords + + +def _count_rows_via_duckdb_parquet(timdex_dataset) -> int: + return timdex_dataset.conn.query(f""" + select count(*) + from read_parquet( + '{timdex_dataset.records.data_root}/**/*.parquet', + hive_partitioning=true + ) + """).fetchone()[0] + + +def _count_parquet_files(timdex_dataset) -> int: + return len( + glob.glob( + f"{timdex_dataset.records.data_root}/**/*.parquet", + recursive=True, + ) + ) + + +def test_records_data_root_created_on_init(timdex_dataset_empty): + expected = f"{timdex_dataset_empty.location.removesuffix('/')}/data/records" + assert os.path.exists(expected) + + +def test_embeddings_data_root_created_on_init(timdex_dataset_empty): + expected = f"{timdex_dataset_empty.location.removesuffix('/')}/data/embeddings" + assert os.path.exists(expected) + assert timdex_dataset_empty.embeddings.data_root == expected def test_dataset_write_records_to_timdex_dataset_empty( timdex_dataset_empty, sample_records_generator ): - written_files = timdex_dataset_empty.write(sample_records_generator(10_000)) + written_files = timdex_dataset_empty.records.write(sample_records_generator(10_000)) assert len(written_files) == 1 assert os.path.exists(timdex_dataset_empty.location) - assert timdex_dataset_empty.dataset.count_rows() == 10_000 + assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 10_000 def test_dataset_write_default_max_rows_per_file( @@ -32,33 +60,18 @@ def test_dataset_write_default_max_rows_per_file( default_max_rows_per_file = timdex_dataset_empty.config.max_rows_per_file total_records = 200_033 - timdex_dataset_empty.write(sample_records_generator(total_records)) + timdex_dataset_empty.records.write(sample_records_generator(total_records)) - assert timdex_dataset_empty.dataset.count_rows() == total_records - assert len(timdex_dataset_empty.dataset.files) == math.ceil( + assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == total_records + assert _count_parquet_files(timdex_dataset_empty) == math.ceil( total_records / default_max_rows_per_file ) -def test_dataset_write_record_batches_uses_batch_size( - timdex_dataset_empty, sample_records_generator -): - total_records = 101 - timdex_dataset_empty.config.write_batch_size = 50 - batches = list( - timdex_dataset_empty.create_record_batches( - sample_records_generator(total_records) - ) - ) - assert len(batches) == math.ceil( - total_records / timdex_dataset_empty.config.write_batch_size - ) - - def test_dataset_write_schema_applied_to_dataset( timdex_dataset_empty, sample_records_generator ): - timdex_dataset_empty.write(sample_records_generator(10)) + timdex_dataset_empty.records.write(sample_records_generator(10)) # manually load dataset to confirm schema without TIMDEXDataset projecting schema # during load @@ -68,13 +81,13 @@ def test_dataset_write_schema_applied_to_dataset( partitioning="hive", ) - assert set(dataset.schema.names) == set(TIMDEX_DATASET_SCHEMA.names) + assert set(dataset.schema.names) == set(TIMDEXRecords.SCHEMA.names) def test_dataset_write_partition_for_single_source( timdex_dataset_empty, sample_records_generator ): - written_files = timdex_dataset_empty.write(sample_records_generator(10)) + written_files = timdex_dataset_empty.records.write(sample_records_generator(10)) assert len(written_files) == 1 assert os.path.exists(timdex_dataset_empty.location) assert "year=2024/month=12/day=01" in written_files[0].path @@ -84,35 +97,41 @@ def test_dataset_write_partition_for_multiple_sources( timdex_dataset_empty, sample_records_generator ): # perform write for source="alma" and run_date="2024-12-01" - written_files_source_a = timdex_dataset_empty.write(sample_records_generator(10)) + written_files_source_a = timdex_dataset_empty.records.write( + sample_records_generator(10) + ) assert os.path.exists(written_files_source_a[0].path) - assert timdex_dataset_empty.dataset.count_rows() == 10 + assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 10 # perform write for source="libguides" and run_date="2024-12-01" - written_files_source_b = timdex_dataset_empty.write( + written_files_source_b = timdex_dataset_empty.records.write( generate_sample_records(num_records=7, source="libguides") ) assert os.path.exists(written_files_source_b[0].path) assert os.path.exists(written_files_source_a[0].path) - assert timdex_dataset_empty.dataset.count_rows() == 17 + assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 17 def test_dataset_write_partition_ignore_existing_data( timdex_dataset_empty, sample_records_generator ): # perform two (2) writes for source="alma" and run_date="2024-12-01" - written_files_source_a0 = timdex_dataset_empty.write(sample_records_generator(10)) - written_files_source_a1 = timdex_dataset_empty.write(sample_records_generator(10)) + written_files_source_a0 = timdex_dataset_empty.records.write( + sample_records_generator(10) + ) + written_files_source_a1 = timdex_dataset_empty.records.write( + sample_records_generator(10) + ) # assert that both files exist and no overwriting occurs assert os.path.exists(written_files_source_a0[0].path) assert os.path.exists(written_files_source_a1[0].path) - assert timdex_dataset_empty.dataset.count_rows() == 20 + assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 20 -@patch("timdex_dataset_api.dataset.uuid.uuid4") +@patch("timdex_dataset_api.data_source.uuid.uuid4") def test_dataset_write_partition_overwrite_files_with_same_name( mock_uuid, timdex_dataset_empty, sample_records_generator ): @@ -125,19 +144,24 @@ def test_dataset_write_partition_overwrite_files_with_same_name( mock_uuid.return_value = "abc" # perform two (2) writes for source="alma" and run_date="2024-12-01" - _ = timdex_dataset_empty.write(sample_records_generator(10)) - written_files_source_a1 = timdex_dataset_empty.write(sample_records_generator(7)) + _ = timdex_dataset_empty.records.write(sample_records_generator(10)) + written_files_source_a1 = timdex_dataset_empty.records.write( + sample_records_generator(7) + ) # assert that only the second file exists and overwriting occurs assert os.path.exists(written_files_source_a1[0].path) - assert timdex_dataset_empty.dataset.count_rows() == 7 + assert _count_rows_via_duckdb_parquet(timdex_dataset_empty) == 7 def test_dataset_write_single_append_delta_success( timdex_dataset_empty, sample_records_generator ): - written_files = timdex_dataset_empty.write(sample_records_generator(1_000)) - append_deltas = os.listdir(timdex_dataset_empty.metadata.append_deltas_path) + written_files = timdex_dataset_empty.records.write(sample_records_generator(1_000)) + records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for( + TIMDEXRecords.METADATA_CONFIG + ) + append_deltas = os.listdir(records_deltas_path) assert len(append_deltas) == len(written_files) @@ -149,8 +173,11 @@ def test_dataset_write_multiple_append_deltas_success( timdex_dataset_empty.config.max_rows_per_file = 100 timdex_dataset_empty.config.max_rows_per_group = 100 - written_files = timdex_dataset_empty.write(sample_records_generator(1_000)) - append_deltas = os.listdir(timdex_dataset_empty.metadata.append_deltas_path) + written_files = timdex_dataset_empty.records.write(sample_records_generator(1_000)) + records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for( + TIMDEXRecords.METADATA_CONFIG + ) + append_deltas = os.listdir(records_deltas_path) assert len(written_files) == 10 assert len(append_deltas) == len(written_files) @@ -159,12 +186,11 @@ def test_dataset_write_multiple_append_deltas_success( def test_dataset_write_append_delta_expected_metadata_columns( timdex_dataset_empty, sample_records_generator ): - timdex_dataset_empty.write(sample_records_generator(1_000)) - append_delta_filepath = os.listdir(timdex_dataset_empty.metadata.append_deltas_path)[ - 0 - ] - - append_delta = pq.ParquetFile( - timdex_dataset_empty.metadata.append_deltas_path / Path(append_delta_filepath) + timdex_dataset_empty.records.write(sample_records_generator(1_000)) + records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for( + TIMDEXRecords.METADATA_CONFIG ) - assert append_delta.schema.names == ORDERED_METADATA_COLUMN_NAMES + append_delta_filepath = os.listdir(records_deltas_path)[0] + + append_delta = pq.ParquetFile(Path(records_deltas_path) / append_delta_filepath) + assert append_delta.schema.names == TIMDEXRecords.METADATA_CONFIG.metadata_columns diff --git a/timdex_dataset_api/__init__.py b/timdex_dataset_api/__init__.py index a3aed52..2633d63 100644 --- a/timdex_dataset_api/__init__.py +++ b/timdex_dataset_api/__init__.py @@ -2,17 +2,27 @@ from importlib.metadata import version +from timdex_dataset_api.data_source import TIMDEXDataSource, ValidTable from timdex_dataset_api.dataset import TIMDEXDataset from timdex_dataset_api.embeddings import DatasetEmbedding, TIMDEXEmbeddings -from timdex_dataset_api.metadata import TIMDEXDatasetMetadata -from timdex_dataset_api.record import DatasetRecord +from timdex_dataset_api.metadata import ( + CurrentMetadataViewSpec, + DataTypeMetadataConfig, + TIMDEXDatasetMetadata, +) +from timdex_dataset_api.records import DatasetRecord, TIMDEXRecords __version__ = version("timdex_dataset_api") __all__ = [ + "CurrentMetadataViewSpec", + "DataTypeMetadataConfig", "DatasetEmbedding", "DatasetRecord", + "TIMDEXDataSource", "TIMDEXDataset", "TIMDEXDatasetMetadata", "TIMDEXEmbeddings", + "TIMDEXRecords", + "ValidTable", ] diff --git a/timdex_dataset_api/data_source.py b/timdex_dataset_api/data_source.py new file mode 100644 index 0000000..1258229 --- /dev/null +++ b/timdex_dataset_api/data_source.py @@ -0,0 +1,586 @@ +"""timdex_dataset_api/data_source.py + +Abstract base class for TIMDEX data sources (records, embeddings, etc.). + +Shared read/write orchestration lives here; subclasses provide schema definitions, +column contracts, and domain-specific hooks. +""" + +import itertools +import logging +import time +import uuid +from abc import ABC +from collections.abc import Iterator +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable + +import pandas as pd +import pyarrow as pa +import pyarrow.dataset as ds + +from timdex_dataset_api.metadata import ( + CurrentMetadataViewSpec, + DataTypeMetadataConfig, + TIMDEXDatasetMetadata, +) + +if TYPE_CHECKING: + from timdex_dataset_api.dataset import TIMDEXDataset + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class ValidTable: + """A table or view that a data source exposes for reading.""" + + name: str + """DuckDB table or view name, e.g. 'current_records'.""" + + description: str + """Human-readable explanation of what this table contains.""" + + +@runtime_checkable +class DataSourceRow(Protocol): + """Protocol for row objects that can be written to a data source.""" + + def to_dict(self) -> dict: ... + + +class TIMDEXDataSource(ABC): + """Abstract base class for TIMDEX data sources. + + Provides shared write, read, and column-contract logic. Subclasses must + define their schema and column contract class variables; metadata + configuration is derived in ``__init_subclass__``. + """ + + # ------------------------------------------------------------------ # + # Required sub-class class vars + # ------------------------------------------------------------------ # + + # Short identifier, e.g. "records", "embeddings", etc. + NAME: ClassVar[str] + + # Full pyarrow schema for parquet files of this data source + SCHEMA: ClassVar[pa.Schema] + + # Location of data parquet files, e.g. "data/records" + DATA_PATH: ClassVar[str] + + # Heavy/data columns read from parquet data files + DATA_COLUMNS: ClassVar[list[str]] + + # Tables and views this data source exposes for reading + VALID_TABLES: ClassVar[list[ValidTable]] + + # ------------------------------------------------------------------ # + # Optional sub-class class vars + # ------------------------------------------------------------------ # + + # Hive-style partition columns (e.g. ``["year", "month", "day"]``) + PARTITION_COLUMNS: ClassVar[list[str]] = [ + "year", + "month", + "day", + ] + + # Current-metadata view specs owned by this data source + CURRENT_VIEW_SPECS: ClassVar[list[CurrentMetadataViewSpec]] = [] + + # Composite key columns used when joining metadata to parquet data. + # filename is always included to physically disambiguate rows that share + # the same logical key but reside in different parquet files (common for + # bolt-on data sources like embeddings). + JOIN_KEYS: ClassVar[list[str]] = [ + "timdex_record_id", + "run_id", + "run_record_offset", + "filename", + ] + + # If True, metadata views are pre-joined to records for base record columns + PREJOIN_RECORDS: ClassVar[bool] = True + + # ------------------------------------------------------------------ # + # Derived class vars + # ------------------------------------------------------------------ # + METADATA_CONFIG: ClassVar[DataTypeMetadataConfig] + ADDITIONAL_METADATA_COLUMNS: ClassVar[list[str]] + DEFAULT_READ_COLUMNS: ClassVar[list[str]] + VALID_READ_COLUMNS: ClassVar[set[str]] + + def __init_subclass__(cls, **kwargs: object) -> None: + """Instantiate DataSource subclasses.""" + super().__init_subclass__(**kwargs) + + # skip derivation for classes that haven't yet declared required contract vars + required_class_vars = [ + "NAME", + "SCHEMA", + "PARTITION_COLUMNS", + "DATA_COLUMNS", + "DATA_PATH", + "VALID_TABLES", + ] + if not all(hasattr(cls, var_name) for var_name in required_class_vars): + return + + cls.ADDITIONAL_METADATA_COLUMNS = cls.derive_additional_metadata_columns( + cls.SCHEMA.names, + cls.DATA_COLUMNS, + TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS, + cls.PARTITION_COLUMNS, + ) + + cls.DEFAULT_READ_COLUMNS = ( + TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS + + cls.ADDITIONAL_METADATA_COLUMNS + + cls.DATA_COLUMNS + ) + + cls.VALID_READ_COLUMNS = set(cls.DEFAULT_READ_COLUMNS) + + cls.METADATA_CONFIG = DataTypeMetadataConfig( + name=cls.NAME, + metadata_columns=cls.derive_metadata_columns( + base_metadata_columns=TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS, + additional_metadata_columns=cls.ADDITIONAL_METADATA_COLUMNS, + prejoin_records_columns=TIMDEXDatasetMetadata.PREJOIN_RECORDS_COLUMNS, + prejoin_records=cls.PREJOIN_RECORDS, + ), + data_path=cls.DATA_PATH, + prejoin_records=cls.PREJOIN_RECORDS, + ) + + def __init__(self, timdex_dataset: "TIMDEXDataset") -> None: + """Instance instantiation; runs after sub-class instantiation.""" + self.timdex_dataset = timdex_dataset + self.schema = self.SCHEMA + self.partition_columns = self.PARTITION_COLUMNS + self._ensure_data_root_exists() + + @property + def data_root(self) -> str: + """Root path for this source's parquet data.""" + return ( + f"{self.timdex_dataset.location.removesuffix('/')}" + f"/{self.METADATA_CONFIG.data_path}" + ) + + @property + def default_table(self) -> str: + """Default table name for read methods.""" + return self.NAME + + @staticmethod + def derive_additional_metadata_columns( + schema_names: list[str], + data_columns: list[str], + base_metadata_columns: list[str], + partition_columns: list[str], + ) -> list[str]: + """Return additional metadata columns for a data source read contract. + + Derives columns from a physical parquet schema by excluding: + - payload/data columns + - shared/base metadata columns + - partition helper columns + """ + return [ + column_name + for column_name in schema_names + if column_name not in data_columns + and column_name not in base_metadata_columns + and column_name not in partition_columns + ] + + @staticmethod + def derive_metadata_columns( + base_metadata_columns: list[str], + additional_metadata_columns: list[str], + prejoin_records_columns: list[str], + *, + prejoin_records: bool, + ) -> list[str]: + """Return metadata columns stored in static/delta metadata tables.""" + if not prejoin_records: + return base_metadata_columns + additional_metadata_columns + + key_columns = [ + column_name + for column_name in base_metadata_columns + if column_name not in prejoin_records_columns and column_name != "filename" + ] + return key_columns + additional_metadata_columns + ["filename"] + + def create_data_structure(self) -> None: + """Ensure source data root exists (idempotent for local datasets).""" + self._ensure_data_root_exists() + + def _ensure_data_root_exists(self) -> None: + """Ensure local data root directory exists for this source.""" + if self.timdex_dataset.location_scheme != "file": + return + Path(self.data_root).mkdir(parents=True, exist_ok=True) + + # ------------------------------------------------------------------ # + # Write pipeline + # ------------------------------------------------------------------ # + + def write( + self, + rows_iter: Iterator[DataSourceRow], + *, + use_threads: bool = True, + write_append_deltas: bool = True, + ) -> list[ds.WrittenFile]: + """Write rows to this data source's parquet dataset. + + Args: + rows_iter: iterator of row objects (DatasetRecord, DatasetEmbedding, etc.) + each must implement ``.to_dict()`` + use_threads: use threads for writing + write_append_deltas: write append deltas for metadata tracking + """ + start_time = time.perf_counter() + written_files: list[ds.WrittenFile] = [] + + filesystem, path = self.timdex_dataset.parse_location(self.data_root) + + batches_iter = self._create_batches(rows_iter) + ds.write_dataset( + batches_iter, + base_dir=path, + basename_template="%s-{i}.parquet" % (str(uuid.uuid4())), # noqa: UP031 + existing_data_behavior="overwrite_or_ignore", + filesystem=filesystem, + file_visitor=lambda written_file: written_files.append(written_file), # type: ignore[arg-type] # noqa: PLW0108 + format="parquet", + max_open_files=500, + max_rows_per_file=self.timdex_dataset.config.max_rows_per_file, + max_rows_per_group=self.timdex_dataset.config.max_rows_per_group, + partitioning=self.partition_columns, + partitioning_flavor="hive", + schema=self.schema, + use_threads=use_threads, + ) + + # write metadata append deltas + if write_append_deltas: + for written_file in written_files: + self.timdex_dataset.metadata.write_append_delta( + written_file.path, # type: ignore[attr-defined] + self.METADATA_CONFIG, + ) + self.timdex_dataset.refresh() + + self.log_write_statistics(start_time, written_files) + + return written_files + + def _create_batches( + self, + rows_iter: Iterator[DataSourceRow], + ) -> Iterator[pa.RecordBatch]: + """Yield ``pyarrow.RecordBatch`` objects from an iterator of row objects.""" + for i, batch in enumerate( + itertools.batched(rows_iter, self.timdex_dataset.config.write_batch_size) + ): + row_dicts = [row.to_dict() for row in batch] + record_batch = pa.RecordBatch.from_pylist(row_dicts) + logger.debug(f"Yielding batch {i + 1} for dataset writing.") + yield record_batch + + def log_write_statistics( + self, + start_time: float, + written_files: list[ds.WrittenFile], + ) -> None: + """Parse written files from write and log statistics.""" + total_time = round(time.perf_counter() - start_time, 2) + total_files = len(written_files) + total_rows = sum( + [wf.metadata.num_rows for wf in written_files] # type: ignore[attr-defined] + ) + total_size = sum([wf.size for wf in written_files]) # type: ignore[attr-defined] + logger.info( + f"Dataset write complete - elapsed: " + f"{total_time}s, " + f"total files: {total_files}, " + f"total rows: {total_rows}, " + f"total size: {total_size}" + ) + + # ------------------------------------------------------------------ # + # Read pipeline + # ------------------------------------------------------------------ # + + def read_batches_iter( + self, + table: str | None = None, + columns: list[str] | None = None, + limit: int | None = None, + where: str | None = None, + **filters: Any, # noqa: ANN401 + ) -> Iterator[pa.RecordBatch]: + """Yield rows as ``pyarrow.RecordBatch`` via metadata-driven two-step reads. + + Args: + table: DuckDB table/view name (defaults to ``self.default_table``) + columns: columns to return (defaults to ``DEFAULT_READ_COLUMNS``) + limit: max rows to yield + where: raw SQL WHERE predicate + **filters: key/value filter pairs + """ + start_time = time.perf_counter() + table = table or self.default_table + + valid_table_names = {vt.name for vt in self.VALID_TABLES} + if table not in valid_table_names: + valid = ", ".join( + f"'{vt.name}' ({vt.description})" for vt in self.VALID_TABLES + ) + raise ValueError(f"Invalid table: '{table}'. Valid tables: {valid}") + + try: + self.timdex_dataset.get_sa_table("metadata", table) + except ValueError as exc: + raise ValueError( + f"Table '{table}' not found in DuckDB context. If this is a new " + f"dataset, either {self.NAME} do not yet exist or a " + "TIMDEXDataset.metadata.rebuild_dataset_metadata() may be required." + ) from exc + + temp_table_name = "read_meta_chunk" + total_yield_count = 0 + metadata_columns = self.timdex_dataset.metadata.get_metadata_columns_for_table( + table + ) + + meta_chunks = self._iter_meta_chunks( + table, + limit=limit, + where=where, + **filters, + ) + for i, meta_chunk_df in enumerate(meta_chunks): + batch_time = time.perf_counter() + batch_yield_count = len(meta_chunk_df) + total_yield_count += batch_yield_count + + self.timdex_dataset.conn.register( + temp_table_name, + meta_chunk_df[metadata_columns], + ) + + try: + data_query = self._build_data_query_for_chunk( + columns, + meta_chunk_df, + registered_metadata_chunk=temp_table_name, + ) + yield from self._iter_data_chunks(data_query) + finally: + self.timdex_dataset.conn.unregister(temp_table_name) + + batch_rps = int(batch_yield_count / (time.perf_counter() - batch_time)) + logger.debug( + f"read_batches_iter batch {i + 1}, " + f"yielded: {batch_yield_count} " + f"@ {batch_rps} records/second, " + f"total yielded: {total_yield_count}" + ) + + logger.debug( + f"read_batches_iter() elapsed: {round(time.perf_counter() - start_time, 2)}s" + ) + + def _iter_meta_chunks( + self, + table: str | None = None, + limit: int | None = None, + where: str | None = None, + **filters: Any, # noqa: ANN401 + ) -> Iterator[pd.DataFrame]: + """Yield pandas DataFrames of metadata query results via keyset pagination.""" + table = table or self.default_table + chunk_size = self.timdex_dataset.config.duckdb_join_batch_size + + keyset_value = (0, 0, 0) + + total_yielded = 0 + while True: + if limit is not None: + remaining = limit - total_yielded + if remaining <= 0: + break + chunk_limit = min(chunk_size, remaining) + else: + chunk_limit = chunk_size + + meta_query = ( + self.timdex_dataset.metadata.build_keyset_paginated_metadata_query( + table, + limit=chunk_limit, + where=where, + keyset_value=keyset_value, + **filters, + ) + ) + meta_chunk_df = self.timdex_dataset.conn.query(meta_query).to_df() + + meta_chunk_count = len(meta_chunk_df) + + if meta_chunk_count == 0: + break + + total_yielded += meta_chunk_count + yield meta_chunk_df + + last_row = meta_chunk_df.iloc[-1] + keyset_value = ( + int(last_row.filename_hash), + int(last_row.run_id_hash), + int(last_row.run_record_offset), + ) + + def _build_data_query_for_chunk( + self, + columns: list[str] | None, + meta_chunk_df: pd.DataFrame, + registered_metadata_chunk: str = "meta_chunk", + ) -> str: + """Build SQL query for data retrieval, joining metadata chunk to parquet.""" + metadata_columns = ( + TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS + self.ADDITIONAL_METADATA_COLUMNS + ) + + requested_columns = columns or self.DEFAULT_READ_COLUMNS + invalid_columns = set(requested_columns) - self.VALID_READ_COLUMNS + if invalid_columns: + invalid = ", ".join(sorted(invalid_columns)) + raise ValueError(f"Invalid column: {invalid}") + + select_parts: list[str] = [] + for column_name in requested_columns: + if column_name in metadata_columns: + select_parts.append(f"mc.{column_name}") + continue + if column_name in self.DATA_COLUMNS: + select_parts.append(f"ds.{column_name}") + + select_cols = ",".join(select_parts) + + filenames = list(meta_chunk_df["filename"].unique()) + if self.timdex_dataset.location_scheme == "s3": + filenames = [ + f"s3://{f.removeprefix('s3://')}" + for f in filenames # type: ignore[union-attr] + ] + parquet_list_sql = "[" + ",".join(f"'{f}'" for f in filenames) + "]" + + rro_values = meta_chunk_df["run_record_offset"].unique() + rro_values.sort() + if len(rro_values) <= 1_000: # noqa: PLR2004 + rro_clause = ( + f"and run_record_offset in ({','.join(str(rro) for rro in rro_values)})" + ) + else: + rro_clause = ( + f"and run_record_offset between {rro_values[0]} and {rro_values[-1]}" + ) + + join_keys = ", ".join(self.JOIN_KEYS) + + return f""" + select + {select_cols} + from read_parquet( + {parquet_list_sql}, + hive_partitioning=true, + filename=true + ) as ds + inner join {registered_metadata_chunk} mc using ( + {join_keys} + ) + where true + {rro_clause}; + """ + + def _iter_data_chunks(self, data_query: str) -> Iterator[pa.RecordBatch]: + """Execute data query and stream ``pyarrow.RecordBatch`` results.""" + if self.timdex_dataset.location_scheme == "s3": + self.timdex_dataset.conn.execute("""set threads=16;""") + try: + cursor = self.timdex_dataset.conn.execute(data_query) + yield from cursor.to_arrow_reader( + batch_size=self.timdex_dataset.config.read_batch_size + ) + finally: + if self.timdex_dataset.location_scheme == "s3": + self.timdex_dataset.conn.execute( + f"""set threads={self.timdex_dataset.conn_factory.threads};""" + ) + + def read_dataframes_iter( + self, + table: str | None = None, + columns: list[str] | None = None, + limit: int | None = None, + where: str | None = None, + **filters: Any, # noqa: ANN401 + ) -> Iterator[pd.DataFrame]: + """Yield rows as pandas DataFrames.""" + for record_batch in self.read_batches_iter( + table=table or self.default_table, + columns=columns, + limit=limit, + where=where, + **filters, + ): + yield record_batch.to_pandas() + + def read_dataframe( + self, + table: str | None = None, + columns: list[str] | None = None, + limit: int | None = None, + where: str | None = None, + **filters: Any, # noqa: ANN401 + ) -> pd.DataFrame | None: + """Read all matching rows into a single pandas DataFrame.""" + df_batches = [ + record_batch.to_pandas() + for record_batch in self.read_batches_iter( + table=table or self.default_table, + columns=columns, + limit=limit, + where=where, + **filters, + ) + ] + if not df_batches: + return None + return pd.concat(df_batches) + + def read_dicts_iter( + self, + table: str | None = None, + columns: list[str] | None = None, + limit: int | None = None, + where: str | None = None, + **filters: Any, # noqa: ANN401 + ) -> Iterator[dict]: + """Yield rows as Python dicts.""" + for record_batch in self.read_batches_iter( + table=table or self.default_table, + columns=columns, + limit=limit, + where=where, + **filters, + ): + yield from record_batch.to_pylist() diff --git a/timdex_dataset_api/dataset.py b/timdex_dataset_api/dataset.py index 66da101..7f37676 100644 --- a/timdex_dataset_api/dataset.py +++ b/timdex_dataset_api/dataset.py @@ -1,72 +1,24 @@ """timdex_dataset_api/dataset.py""" -import itertools -import json import os import time -import uuid -from collections.abc import Iterator from dataclasses import dataclass, field -from datetime import date, datetime -from pathlib import Path -from typing import TYPE_CHECKING, Literal, TypedDict, Unpack +from typing import Literal from urllib.parse import urlparse import boto3 -import pandas as pd -import pyarrow as pa -import pyarrow.dataset as ds from duckdb_engine import ConnectionWrapper from pyarrow import fs from sqlalchemy import MetaData, Table, create_engine -from sqlalchemy.types import ARRAY, FLOAT from timdex_dataset_api.config import configure_logger from timdex_dataset_api.embeddings import TIMDEXEmbeddings from timdex_dataset_api.metadata import TIMDEXDatasetMetadata +from timdex_dataset_api.records import TIMDEXRecords from timdex_dataset_api.utils import DuckDBConnectionFactory -if TYPE_CHECKING: - from timdex_dataset_api.record import DatasetRecord # pragma: nocover - - logger = configure_logger(__name__) -TIMDEX_DATASET_SCHEMA = pa.schema( - ( - pa.field("timdex_record_id", pa.string()), - pa.field("source_record", pa.binary()), - pa.field("transformed_record", pa.binary()), - pa.field("source", pa.string()), - pa.field("run_date", pa.date32()), - pa.field("run_type", pa.string()), - pa.field("action", pa.string()), - pa.field("run_id", pa.string()), - pa.field("run_record_offset", pa.int32()), - pa.field("year", pa.string()), - pa.field("month", pa.string()), - pa.field("day", pa.string()), - pa.field("run_timestamp", pa.timestamp("us", tz="UTC")), - ) -) - -TIMDEX_DATASET_PARTITION_COLUMNS = [ - "year", - "month", - "day", -] - - -class DatasetFilters(TypedDict, total=False): - timdex_record_id: str | list[str] | None - source: str | list[str] | None - run_date: str | date | list[str | date] | None - run_type: str | list[str] | None - action: str | list[str] | None - run_id: str | list[str] | None - run_record_offset: int | list[int] | None - run_timestamp: str | datetime | list[str | datetime] | None - @dataclass class TIMDEXDatasetConfig: @@ -111,6 +63,8 @@ class TIMDEXDatasetConfig: class TIMDEXDataset: + """Class to represent the TIMDEXDataset.""" + def __init__( self, location: str, @@ -131,13 +85,6 @@ def __init__( self.location = location self.preload_current_records = preload_current_records - self.create_data_structure() - - # pyarrow dataset - self.schema = TIMDEX_DATASET_SCHEMA - self.partition_columns = TIMDEX_DATASET_PARTITION_COLUMNS - self.dataset = self.load_pyarrow_dataset() - # create DuckDB connection used by all classes self.conn_factory = DuckDBConnectionFactory(location_scheme=self.location_scheme) self.conn = self.conn_factory.create_connection() @@ -145,7 +92,22 @@ def __init__( # create schemas self._create_duckdb_schemas() + source_classes = [TIMDEXRecords, TIMDEXEmbeddings] + + # define which data types participate in metadata + self.data_type_configs = [ + source_class.METADATA_CONFIG for source_class in source_classes + ] + + # define current-row semantics for metadata current_* views + self.current_metadata_view_specs = [ + current_view_spec + for source_class in source_classes + for current_view_spec in source_class.CURRENT_VIEW_SPECS + ] + # composed components receive self + self.records = TIMDEXRecords(self) self.metadata = TIMDEXDatasetMetadata(self) self.embeddings = TIMDEXEmbeddings(self) @@ -162,10 +124,6 @@ def location_scheme(self) -> Literal["file", "s3"]: return "s3" raise ValueError(f"Location with scheme type '{scheme}' not supported.") - @property - def data_records_root(self) -> str: - return f"{self.location.removesuffix('/')}/data/records" # type: ignore[union-attr] - def refresh(self) -> None: """Refresh dataset by fully reinitializing.""" self.__init__( # type: ignore[misc] @@ -174,47 +132,6 @@ def refresh(self) -> None: preload_current_records=self.preload_current_records, ) - def create_data_structure(self) -> None: - """Ensure ETL records data structure exists in TIMDEX dataset.""" - if self.location_scheme == "file": - Path(self.data_records_root).mkdir( - parents=True, - exist_ok=True, - ) - - def load_pyarrow_dataset(self, parquet_files: list[str] | None = None) -> ds.Dataset: - """Lazy load a pyarrow.dataset.Dataset. - - The dataset is loaded via the expected schema as defined by module constant - TIMDEX_DATASET_SCHEMA. If the target dataset differs in any way, errors may be - raised when reading or writing data. - - Args: - parquet_files: explicit list of parquet files to construct pyarrow dataset - """ - start_time = time.perf_counter() - - # get pyarrow filesystem and dataset path basesd on self.location - filesystem, path = self.parse_location(self.data_records_root) - - # set source for pyarrow dataset - source: str | list[str] = parquet_files or path - - dataset = ds.dataset( - source, - schema=self.schema, - format="parquet", - partitioning="hive", - filesystem=filesystem, - ) - - logger.info( - f"Dataset successfully loaded: '{self.data_records_root}', " - f"{round(time.perf_counter() - start_time, 2)}s" - ) - - return dataset - def parse_location( self, location: str, @@ -257,7 +174,6 @@ def get_s3_filesystem() -> fs.FileSystem: def _create_duckdb_schemas(self) -> None: """Create DuckDB schemas used by all components.""" self.conn.execute("create schema metadata;") - self.conn.execute("create schema data;") def reflect_sa_tables(self, schemas: list[str] | None = None) -> None: """Reflect SQLAlchemy metadata for DuckDB schemas. @@ -266,10 +182,10 @@ def reflect_sa_tables(self, schemas: list[str] | None = None) -> None: are stored in self.sa_tables as {schema: {table_name: Table}}. Args: - schemas: list of schemas to reflect; defaults to ["metadata", "data"] + schemas: list of schemas to reflect; defaults to ["metadata"] """ start_time = time.perf_counter() - schemas = schemas or ["metadata", "data"] + schemas = schemas or ["metadata"] engine = create_engine( "duckdb://", @@ -280,16 +196,11 @@ def reflect_sa_tables(self, schemas: list[str] | None = None) -> None: db_metadata = MetaData() db_metadata.reflect(bind=engine, schema=schema, views=True) - # store tables in flat dict keyed by table name (without schema prefix) self.sa_tables[schema] = { table_name.removeprefix(f"{schema}."): table for table_name, table in db_metadata.tables.items() } - # type fixup for embedding_vector column (DuckDB LIST -> SA ARRAY) - if "embeddings" in self.sa_tables.get("data", {}): - self.sa_tables["data"]["embeddings"].c.embedding_vector.type = ARRAY(FLOAT) - logger.debug( f"SQLAlchemy reflection complete for schemas {schemas}, " f"{round(time.perf_counter() - start_time, 3)}s" @@ -302,400 +213,3 @@ def get_sa_table(self, schema: str, table: str) -> Table: if table not in self.sa_tables[schema]: raise ValueError(f"Table '{table}' not found in schema '{schema}'.") return self.sa_tables[schema][table] - - def write( - self, - records_iter: Iterator["DatasetRecord"], - *, - use_threads: bool = True, - write_append_deltas: bool = True, - ) -> list[ds.WrittenFile]: - """Write records to the TIMDEX parquet dataset. - - This method expects an iterator of DatasetRecord instances. - - This method encapsulates all dataset writing mechanics and performance - optimizations (e.g. batching) so that the calling context can focus on yielding - data. - - This method uses the configuration existing_data_behavior="overwrite_or_ignore", - which will ignore any existing data and will overwrite files with the same name - as the parquet file. Since a UUID is generated for each write via the - basename_template, this effectively makes a write idempotent to the - TIMDEX dataset. - - A max_open_files=500 configuration is set to avoid AWS S3 503 error "SLOW_DOWN" - if too many PutObject calls are made in parallel. Testing suggests this does not - substantially slow down the overall write. - - Args: - - records_iter: Iterator of DatasetRecord instances - - use_threads: boolean if threads should be used for writing - - write_append_deltas: boolean if append deltas should be written for records - written during write - """ - start_time = time.perf_counter() - written_files: list[ds.WrittenFile] = [] - - filesystem, path = self.parse_location(self.data_records_root) - - # write ETL parquet records - record_batches_iter = self.create_record_batches(records_iter) - ds.write_dataset( - record_batches_iter, - base_dir=path, - basename_template="%s-{i}.parquet" % (str(uuid.uuid4())), # noqa: UP031 - existing_data_behavior="overwrite_or_ignore", - filesystem=filesystem, - file_visitor=lambda written_file: written_files.append(written_file), # type: ignore[arg-type] # noqa: PLW0108 - format="parquet", - max_open_files=500, - max_rows_per_file=self.config.max_rows_per_file, - max_rows_per_group=self.config.max_rows_per_group, - partitioning=self.partition_columns, - partitioning_flavor="hive", - schema=self.schema, - use_threads=use_threads, - ) - - # refresh dataset files - self.dataset = self.load_pyarrow_dataset() - - # write metadata append deltas - if write_append_deltas: - for written_file in written_files: - self.metadata.write_append_delta_duckdb(written_file.path) # type: ignore[attr-defined] - self.refresh() - - self.log_write_statistics(start_time, written_files) - - return written_files - - def create_record_batches( - self, records_iter: Iterator["DatasetRecord"] - ) -> Iterator[pa.RecordBatch]: - """Yield pyarrow.RecordBatches for writing. - - This method expects an iterator of DatasetRecord instances. - - Each DatasetRecord is serialized to a dictionary, any column data shared by all - rows is added to the record, and then added to a pyarrow.RecordBatch for writing. - - Args: - - records_iter: Iterator of DatasetRecord instances - """ - for i, record_batch in enumerate( - itertools.batched(records_iter, self.config.write_batch_size) - ): - record_dicts = [record.to_dict() for record in record_batch] - batch = pa.RecordBatch.from_pylist(record_dicts) - logger.debug(f"Yielding batch {i + 1} for dataset writing.") - yield batch - - def log_write_statistics( - self, - start_time: float, - written_files: list[ds.WrittenFile], - ) -> None: - """Parse written files from write and log statistics.""" - total_time = round(time.perf_counter() - start_time, 2) - total_files = len(written_files) - total_rows = sum( - [wf.metadata.num_rows for wf in written_files] # type: ignore[attr-defined] - ) - total_size = sum([wf.size for wf in written_files]) # type: ignore[attr-defined] - logger.info( - f"Dataset write complete - elapsed: " - f"{total_time}s, " - f"total files: {total_files}, " - f"total rows: {total_rows}, " - f"total size: {total_size}" - ) - - def read_batches_iter( - self, - table: str = "records", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[DatasetFilters], - ) -> Iterator[pa.RecordBatch]: - """Yield ETL records as pyarrow.RecordBatches. - - This is the base read method. All read methods eventually drop down and use this - for streaming batches of records. This method performs a two-step process: - - 1. Perform a "metadata" query that narrows down records and physical parquet - files to read from. - 2. Perform a "data" query that retrieves actual rows, joining the metadata - information to increase efficiency. - - More detail can be found here: docs/reading.md - - Args: - - table: an available DuckDB view or table - - columns: list of columns to return - - limit: limit number of records yielded - - where: raw SQL WHERE clause that can be used alone, or in combination with - key/value DatasetFilters - - filters: simple filtering based on key/value pairs from DatasetFilters - """ - start_time = time.perf_counter() - - # ensure valid table - if table not in ["records", "current_records"]: - raise ValueError(f"Invalid table: '{table}'") - - # ensure table exists - try: - self.get_sa_table("metadata", table) - except ValueError as exc: - raise ValueError( - f"Table '{table}' not found in DuckDB context. If this is a new " - "dataset, either records do not yet exist or a " - "TIMDEXDataset.metadata.rebuild_dataset_metadata() may be required." - ) from exc - - temp_table_name = "read_meta_chunk" - total_yield_count = 0 - - meta_chunks = self._iter_meta_chunks( - table, - limit=limit, - where=where, - **filters, - ) - for i, meta_chunk_df in enumerate(meta_chunks): - batch_time = time.perf_counter() - batch_yield_count = len(meta_chunk_df) - total_yield_count += batch_yield_count - - self.conn.register( - temp_table_name, - meta_chunk_df[ - [ - "timdex_record_id", - "run_id", - "run_record_offset", - ] - ], - ) - - # build and perform data query, yield records - # set in try/finally block to ensure we always deregister the meta table - try: - data_query = self._build_data_query_for_chunk( - columns, - meta_chunk_df, - registered_metadata_chunk=temp_table_name, - ) - yield from self._iter_data_chunks(data_query) - finally: - self.conn.unregister(temp_table_name) - - batch_rps = int(batch_yield_count / (time.perf_counter() - batch_time)) - logger.debug( - f"read_batches_iter batch {i + 1}, yielded: {batch_yield_count} " - f"@ {batch_rps} records/second, total yielded: {total_yield_count}" - ) - - logger.debug( - f"read_batches_iter() elapsed: {round(time.perf_counter() - start_time, 2)}s" - ) - - def _iter_meta_chunks( - self, - table: str = "records", - limit: int | None = None, - where: str | None = None, - **filters: Unpack[DatasetFilters], - ) -> Iterator[pd.DataFrame]: - """Utility method to yield pandas Dataframe chunks of metadata query results. - - The approach here is to use "keyset" pagination, which means each paged result - is a greater-than (>) check against a tuple of ordered values from the previous - chunk. This is more performant than a LIMIT + OFFSET. - """ - # use duckdb_join_batch_size as the chunk size for keyset pagination - chunk_size = self.config.duckdb_join_batch_size - - # init keyset value of zeros to begin with - keyset_value = (0, 0, 0) - - total_yielded = 0 - while True: - # enforce limit if passed - if limit is not None: - remaining = limit - total_yielded - if remaining <= 0: - break - chunk_limit = min(chunk_size, remaining) - else: - chunk_limit = chunk_size - - # perform chunk query and convert to pyarrow Table - meta_query = self.metadata.build_keyset_paginated_metadata_query( - table, - limit=chunk_limit, # pass chunk_limit instead of limit - where=where, - keyset_value=keyset_value, - **filters, - ) - meta_chunk_df = self.metadata.conn.query(meta_query).to_df() - - meta_chunk_count = len(meta_chunk_df) - - # an empty chunk signals end of pagination - if meta_chunk_count == 0: - break - - # yield this chunk of data - total_yielded += meta_chunk_count - yield meta_chunk_df[ - [ - "timdex_record_id", - "run_id", - "run_record_offset", - "filename", - ] - ] - - # update keyset value using the last row from this chunk - last_row = meta_chunk_df.iloc[-1] - keyset_value = ( - int(last_row.filename_hash), - int(last_row.run_id_hash), - int(last_row.run_record_offset), - ) - - def _build_data_query_for_chunk( - self, - columns: list[str] | None, - meta_chunk_df: pd.DataFrame, - registered_metadata_chunk: str = "meta_chunk", - ) -> str: - """Build SQL query used for data retrieval, joining on passed metadata data.""" - # build select columns - select_cols = ",".join( - [f"ds.{col}" for col in (columns or TIMDEX_DATASET_SCHEMA.names)] - ) - - # build list of explicit parquet files to read from - filenames = list(meta_chunk_df["filename"].unique()) - if self.location_scheme == "s3": - filenames = [ - f"s3://{f.removeprefix('s3://')}" - for f in filenames # type: ignore[union-attr] - ] - parquet_list_sql = "[" + ",".join(f"'{f}'" for f in filenames) + "]" - - # build run_record_offset WHERE clause to leverage row group pruning - rro_values = meta_chunk_df["run_record_offset"].unique() - rro_values.sort() - if len(rro_values) <= 1_000: # noqa: PLR2004 - rro_clause = ( - f"and run_record_offset in ({','.join(str(rro) for rro in rro_values)})" - ) - else: - rro_clause = ( - f"and run_record_offset between {rro_values[0]} and {rro_values[-1]}" - ) - - return f""" - select - {select_cols} - from read_parquet( - {parquet_list_sql}, - hive_partitioning=true, - filename=true - ) as ds - inner join {registered_metadata_chunk} mc using ( - timdex_record_id, run_id, run_record_offset - ) - where true - {rro_clause}; - """ - - def _iter_data_chunks(self, data_query: str) -> Iterator[pa.RecordBatch]: - """Perform a query to retrieve data and stream chunks.""" - if self.location_scheme == "s3": - self.conn.execute("""set threads=16;""") - try: - cursor = self.conn.execute(data_query) - yield from cursor.to_arrow_reader(batch_size=self.config.read_batch_size) - finally: - if self.location_scheme == "s3": - self.conn.execute(f"""set threads={self.conn_factory.threads};""") - - def read_dataframes_iter( - self, - table: str = "records", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[DatasetFilters], - ) -> Iterator[pd.DataFrame]: - for record_batch in self.read_batches_iter( - table=table, - columns=columns, - limit=limit, - where=where, - **filters, - ): - yield record_batch.to_pandas() - - def read_dataframe( - self, - table: str = "records", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[DatasetFilters], - ) -> pd.DataFrame | None: - df_batches = [ - record_batch.to_pandas() - for record_batch in self.read_batches_iter( - table=table, - columns=columns, - limit=limit, - where=where, - **filters, - ) - ] - if not df_batches: - return None - return pd.concat(df_batches) - - def read_dicts_iter( - self, - table: str = "records", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[DatasetFilters], - ) -> Iterator[dict]: - for record_batch in self.read_batches_iter( - table=table, - columns=columns, - limit=limit, - where=where, - **filters, - ): - yield from record_batch.to_pylist() - - def read_transformed_records_iter( - self, - table: str = "records", - limit: int | None = None, - where: str | None = None, - **filters: Unpack[DatasetFilters], - ) -> Iterator[dict]: - for record_dict in self.read_dicts_iter( - table=table, - columns=["transformed_record"], - limit=limit, - where=where, - **filters, - ): - if transformed_record := record_dict["transformed_record"]: - yield json.loads(transformed_record) diff --git a/timdex_dataset_api/embeddings.py b/timdex_dataset_api/embeddings.py index b91df56..cc01325 100644 --- a/timdex_dataset_api/embeddings.py +++ b/timdex_dataset_api/embeddings.py @@ -1,80 +1,13 @@ -import itertools -import logging -import time -import uuid -from collections.abc import Iterator -from datetime import UTC, date, datetime -from typing import TYPE_CHECKING, TypedDict, Unpack, cast +from datetime import UTC, datetime +from typing import ClassVar import attrs -import pandas as pd import pyarrow as pa -import pyarrow.dataset as ds from attrs import asdict, define, field -from duckdb import DuckDBPyConnection -from duckdb import IOException as DuckDBIOException -from duckdb_engine import Dialect as DuckDBDialect -from sqlalchemy import and_, select, text -from timdex_dataset_api.record import datetime_iso_parse -from timdex_dataset_api.utils import build_filter_expr_sa - -if TYPE_CHECKING: - from timdex_dataset_api import TIMDEXDataset - - -logger = logging.getLogger(__name__) - -TIMDEX_DATASET_EMBEDDINGS_SCHEMA = pa.schema( - ( - pa.field("timdex_record_id", pa.string()), - pa.field("run_id", pa.string()), - pa.field("run_record_offset", pa.int32()), - pa.field("embedding_timestamp", pa.timestamp("us", tz="UTC")), - pa.field("embedding_model", pa.string()), - pa.field("embedding_strategy", pa.string()), - pa.field("embedding_vector", pa.list_(pa.float32())), - pa.field("embedding_object", pa.binary()), - pa.field("year", pa.string()), - pa.field("month", pa.string()), - pa.field("day", pa.string()), - ) -) - - -EMBEDDINGS_FILTER_COLUMNS = { - "timdex_record_id", - "run_id", - "run_record_offset", - "embedding_timestamp", - "embedding_model", - "embedding_strategy", -} - -# subset of record metadata columns for filtering and selecting -METADATA_SELECT_FILTER_COLUMNS = { - "source", - "run_date", - "run_type", - "action", - "run_timestamp", -} - - -class EmbeddingsFilters(TypedDict, total=False): - # embeddings columns - timdex_record_id: str - run_id: str - run_record_offset: int - embedding_timestamp: str | datetime - embedding_model: str - embedding_strategy: str - # record metadata columns - source: str | list[str] - run_date: str | date | list[str | date] - run_type: str | list[str] - action: str | list[str] - run_timestamp: str | datetime | list[str | datetime] +from timdex_dataset_api.data_source import TIMDEXDataSource, ValidTable +from timdex_dataset_api.metadata import CurrentMetadataViewSpec +from timdex_dataset_api.utils import datetime_iso_parse @define @@ -135,382 +68,145 @@ def to_dict( } -class TIMDEXEmbeddings: - def __init__(self, timdex_dataset: "TIMDEXDataset"): - """Init TIMDEXEmbeddings. - - Class to handle the writing and readings of embeddings associated with TIMDEX - records. - - Args: - - timdex_dataset: instance of TIMDEXDataset - """ - self.timdex_dataset = timdex_dataset - self.conn = timdex_dataset.conn - - self.schema = TIMDEX_DATASET_EMBEDDINGS_SCHEMA - self.partition_columns = ["year", "month", "day"] - - # set up embeddings views - self._setup_embeddings_views() - - @property - def data_embeddings_root(self) -> str: - return f"{self.timdex_dataset.location.removesuffix('/')}/data/embeddings" - - def _setup_embeddings_views(self) -> None: - """Set up embeddings views in the 'data' schema.""" - start_time = time.perf_counter() - - try: - self._create_embeddings_view(self.conn) - self._create_current_embeddings_view(self.conn) - self._create_current_run_embeddings_view(self.conn) - except DuckDBIOException: - logger.debug("No embeddings parquet files found") - except Exception as exception: # noqa: BLE001 - logger.warning(f"Error creating embeddings views: {exception}") - - logger.debug( - "Embeddings views setup for TIMDEXEmbeddings, " - f"{round(time.perf_counter() - start_time, 2)}s" +class TIMDEXEmbeddings(TIMDEXDataSource): + """Class to handle record embeddings in the TIMDEXDataset.""" + + NAME: ClassVar[str] = "embeddings" + + SCHEMA: ClassVar[pa.Schema] = pa.schema( + ( + pa.field("timdex_record_id", pa.string()), + pa.field("run_id", pa.string()), + pa.field("run_record_offset", pa.int32()), + pa.field("embedding_timestamp", pa.timestamp("us", tz="UTC")), + pa.field("embedding_model", pa.string()), + pa.field("embedding_strategy", pa.string()), + pa.field("embedding_vector", pa.list_(pa.float32())), + pa.field("embedding_object", pa.binary()), + pa.field("year", pa.string()), + pa.field("month", pa.string()), + pa.field("day", pa.string()), ) + ) - def _create_embeddings_view(self, conn: DuckDBPyConnection) -> None: - """Create a view that projects over embeddings parquet files.""" - logger.debug("creating view data.embeddings") - - conn.execute(f""" - create or replace view data.embeddings as - ( - select * - from read_parquet( - '{self.data_embeddings_root}/**/*.parquet', - hive_partitioning=true, - filename=true - ) - ); - """) - - def _create_current_embeddings_view(self, conn: DuckDBPyConnection) -> None: - """Create a view of current embedding records. - - This builds on the 'data.embeddings' view. This view includes only - the most current version of each embedding grouped by - [timdex_record_id, embedding_strategy]. - """ - logger.debug("creating view data.current_embeddings") - - # SQL for the current records logic (CTEs) - conn.execute(""" - create or replace view data.current_embeddings as + DATA_COLUMNS: ClassVar[list[str]] = [ + "embedding_vector", + "embedding_object", + ] + + DATA_PATH: ClassVar[str] = "data/embeddings" + + VALID_TABLES: ClassVar[list[ValidTable]] = [ + ValidTable( + name="embeddings", + description="All embedding versions across all runs.", + ), + ValidTable( + name="current_embeddings", + description=( + "One row per (timdex_record_id, embedding_model," + " embedding_strategy) representing the most recent" + " embedding for each current record." + ), + ), + ValidTable( + name="current_run_embeddings", + description=( + "One row per (timdex_record_id, run_id, embedding_model," + " embedding_strategy) representing the most recent" + " embedding within each run, regardless of whether the" + " record is current." + ), + ), + ] + + CURRENT_METADATA_VIEW_QUERY: ClassVar[str] = """ + with + -- CTE of embeddings attached to current record versions only + ce_current_record_embeddings as ( - with - -- CTE of embeddings ranked by embedding_timestamp - ce_ranked_embeddings as - ( - select - *, - row_number() over ( - partition by timdex_record_id, embedding_strategy - order by - embedding_timestamp desc nulls last, - run_record_offset desc nulls last - ) as rn - from data.embeddings - ) - -- final select for current records (rn = 1) select - * exclude (rn) - from ce_ranked_embeddings - where rn = 1 - ); - """) - - def _create_current_run_embeddings_view(self, conn: DuckDBPyConnection) -> None: - """Create a view of current embedding records per run. - - This builds on the 'data.embeddings' view. This view includes only - the most current version of each embedding per run grouped by - [timdex_record_id, run_id, embedding_strategy,]. - """ - logger.debug("creating view data.current_run_embeddings") + e.* + from metadata.embeddings e + join metadata.current_records r using ( + source, + timdex_record_id, + run_id, + run_record_offset + ) + ), - # SQL for the current records logic (CTEs) - conn.execute(""" - create or replace view data.current_run_embeddings as + -- CTE of current-record embeddings ranked by embedding recency + ce_ranked_embeddings as ( - with - -- CTE of embeddings ranked by embedding_timestamp - ce_ranked_embeddings as - ( - select - *, - row_number() over ( - partition by timdex_record_id, run_id, embedding_strategy - order by - embedding_timestamp desc nulls last, - run_id desc nulls last, - run_record_offset desc nulls last - ) as rn - from data.embeddings - ) - -- final select for current records (rn = 1) select - * exclude (rn) - from ce_ranked_embeddings - where rn = 1 - ); - """) - - def write( - self, - embeddings_iter: Iterator[DatasetEmbedding], - *, - use_threads: bool = True, - ) -> list[ds.WrittenFile]: - """Write embeddings as parquet files to /data/embeddings. - - Approach is similar to TIMDEXDataset.write() for Records: - - use self.data_embeddings_root for location of embeddings parquet files - - use pyarrow Dataset to write rows - """ - start_time = time.perf_counter() - written_files: list[ds.WrittenFile] = [] - - filesystem, path = self.timdex_dataset.parse_location(self.data_embeddings_root) - - embedding_batches_iter = self.create_embedding_batches(embeddings_iter) - ds.write_dataset( - embedding_batches_iter, - base_dir=path, - basename_template="%s-{i}.parquet" % (str(uuid.uuid4())), # noqa: UP031 - existing_data_behavior="overwrite_or_ignore", - filesystem=filesystem, - file_visitor=lambda written_file: written_files.append(written_file), # type: ignore[arg-type] # noqa: PLW0108 - format="parquet", - max_open_files=500, - max_rows_per_file=self.timdex_dataset.config.max_rows_per_file, - max_rows_per_group=self.timdex_dataset.config.max_rows_per_group, - partitioning=self.partition_columns, - partitioning_flavor="hive", - schema=self.schema, - use_threads=use_threads, - ) - - self.log_write_statistics(start_time, written_files) - - return written_files - - def create_embedding_batches( - self, embeddings_iter: Iterator["DatasetEmbedding"] - ) -> Iterator[pa.RecordBatch]: - for i, embedding_batch in enumerate( - itertools.batched( - embeddings_iter, self.timdex_dataset.config.write_batch_size + e.*, + row_number() over ( + partition by + e.timdex_record_id, + e.embedding_model, + e.embedding_strategy + order by + e.embedding_timestamp desc nulls last, + e.filename desc nulls last + ) as rn + from ce_current_record_embeddings e ) - ): - embedding_dicts = [embedding.to_dict() for embedding in embedding_batch] - batch = pa.RecordBatch.from_pylist(embedding_dicts) - logger.debug(f"Yielding batch {i + 1} for dataset writing.") - yield batch + -- final select for current embeddings (rn = 1) + select + * exclude (rn) + from ce_ranked_embeddings + where rn = 1 + """ - def log_write_statistics( - self, - start_time: float, - written_files: list[ds.WrittenFile], - ) -> None: - """Parse written files from write and log statistics.""" - total_time = round(time.perf_counter() - start_time, 2) - total_files = len(written_files) - total_rows = sum( - [wf.metadata.num_rows for wf in written_files] # type: ignore[attr-defined] - ) - total_size = sum([wf.size for wf in written_files]) # type: ignore[attr-defined] - logger.info( - f"Dataset write complete - elapsed: " - f"{total_time}s, " - f"total files: {total_files}, " - f"total rows: {total_rows}, " - f"total size: {total_size}" + CURRENT_METADATA_VIEW_SPEC: ClassVar[CurrentMetadataViewSpec] = ( + CurrentMetadataViewSpec( + name="current_embeddings", + query_sql=CURRENT_METADATA_VIEW_QUERY, + required_metadata_tables=["embeddings", "current_records"], ) + ) - def read_batches_iter( - self, - table: str = "embeddings", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[EmbeddingsFilters], - ) -> Iterator[pa.RecordBatch]: - """Yield ETL records as pyarrow.RecordBatches. - - This is the base read method. All read methods use this for streaming - batches of records. This method relies on DuckDB to project over all - embeddings parquet files (i.e., no "metadata layer") and filter data. - """ - start_time = time.perf_counter() - - if table not in ["embeddings", "current_embeddings", "current_run_embeddings"]: - raise ValueError(f"Invalid table: '{table}'") - - # ensure table exists - try: - self.timdex_dataset.get_sa_table("data", table) - except ValueError: - logger.warning( - f"Table '{table}' not found in DuckDB context. Embeddings may not yet " - "exist or TIMDEXDataset.refresh() may be required." + CURRENT_RUN_METADATA_VIEW_QUERY: ClassVar[str] = """ + with + -- CTE of embeddings ranked by embedding recency within a run and family + -- keep run_timestamp because the same run_id can be written more than once + -- keep run_record_offset as an intra-run tie-break when the same logical + -- record appears more than once within a run + crce_ranked_embeddings as + ( + select + e.*, + row_number() over ( + partition by + e.timdex_record_id, + e.run_id, + e.embedding_model, + e.embedding_strategy + order by + e.run_timestamp desc nulls last, + e.embedding_timestamp desc nulls last, + e.run_record_offset desc nulls last, + e.filename desc nulls last + ) as rn + from metadata.embeddings e ) - return - - data_query = self._build_query( - table, - columns, - limit, - where, - **filters, - ) - cursor = self.conn.execute(data_query) - yield from cursor.to_arrow_reader( - batch_size=self.timdex_dataset.config.read_batch_size - ) - - logger.debug(f"read() elapsed: {round(time.perf_counter() - start_time, 2)}s") - - def _build_query( - self, - table: str = "embeddings", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[EmbeddingsFilters], - ) -> str: - """Build SQL query using SQLAlchemy. - - The method returns a SQL query string, which SQLAlchemy executes to - fetch results. Always joins to metadata.records to enable filtering - by metadata columns (source, run_date, run_type, action, run_timestamp). - """ - embeddings_table = self.timdex_dataset.get_sa_table("data", table) - metadata_table = self.timdex_dataset.get_sa_table("metadata", "records") - - # select specific columns or default to all from embeddings + metadata - if columns: - embeddings_cols = [] - metadata_cols = [] - - for col_name in columns: - if col_name in TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names: - embeddings_cols.append(embeddings_table.c[col_name]) - elif col_name in METADATA_SELECT_FILTER_COLUMNS: - metadata_cols.append(metadata_table.c[col_name]) - else: - raise ValueError(f"Invalid column: {col_name}") - - stmt = select(*embeddings_cols, *metadata_cols) - else: - embeddings_cols = [ - embeddings_table.c[col] for col in TIMDEX_DATASET_EMBEDDINGS_SCHEMA.names - ] - metadata_cols = [ - metadata_table.c[col] for col in METADATA_SELECT_FILTER_COLUMNS - ] - stmt = select(*embeddings_cols, *metadata_cols) - - # create SQL statement with join to metadata.records - join_condition = and_( - embeddings_table.c.timdex_record_id == metadata_table.c.timdex_record_id, - embeddings_table.c.run_id == metadata_table.c.run_id, - embeddings_table.c.run_record_offset == metadata_table.c.run_record_offset, - ) - stmt = stmt.select_from(embeddings_table.join(metadata_table, join_condition)) - - # split filters between embeddings and metadata tables - embeddings_filters = { - k: v for k, v in filters.items() if k in EMBEDDINGS_FILTER_COLUMNS - } - record_metadata_filters = { - k: v for k, v in filters.items() if k in METADATA_SELECT_FILTER_COLUMNS - } - - # apply embeddings filters - embeddings_filter_expr = build_filter_expr_sa( - embeddings_table, **cast("dict", embeddings_filters) - ) - if embeddings_filter_expr is not None: - stmt = stmt.where(embeddings_filter_expr) - - # apply metadata filters - record_metadata_filter_expr = build_filter_expr_sa( - metadata_table, **cast("dict", record_metadata_filters) - ) - if record_metadata_filter_expr is not None: - stmt = stmt.where(record_metadata_filter_expr) - - # explicit raw WHERE string - if where is not None and where.strip(): - stmt = stmt.where(text(where)) - - # apply limit if present - if limit: - stmt = stmt.limit(limit) + -- final select for current run embeddings (rn = 1) + select + * exclude (rn) + from crce_ranked_embeddings + where rn = 1 + """ - # using DuckDB dialect, compile to SQL string - compiled = stmt.compile( - dialect=DuckDBDialect(), - compile_kwargs={"literal_binds": True}, + CURRENT_RUN_METADATA_VIEW_SPEC: ClassVar[CurrentMetadataViewSpec] = ( + CurrentMetadataViewSpec( + name="current_run_embeddings", + query_sql=CURRENT_RUN_METADATA_VIEW_QUERY, + required_metadata_tables=["embeddings", "records"], ) - return str(compiled) - - def read_dataframes_iter( - self, - table: str = "embeddings", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[EmbeddingsFilters], - ) -> Iterator[pd.DataFrame]: - for record_batch in self.read_batches_iter( - table=table, columns=columns, limit=limit, where=where, **filters - ): - yield record_batch.to_pandas() - - def read_dataframe( - self, - table: str = "embeddings", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[EmbeddingsFilters], - ) -> pd.DataFrame | None: - df_batches = [ - record_batch.to_pandas() - for record_batch in self.read_batches_iter( - table=table, - columns=columns, - limit=limit, - where=where, - **filters, - ) - ] - if not df_batches: - return None - return pd.concat(df_batches) + ) - def read_dicts_iter( - self, - table: str = "embeddings", - columns: list[str] | None = None, - limit: int | None = None, - where: str | None = None, - **filters: Unpack[EmbeddingsFilters], - ) -> Iterator[dict]: - for record_batch in self.read_batches_iter( - table=table, - columns=columns, - limit=limit, - where=where, - **filters, - ): - yield from record_batch.to_pylist() + CURRENT_VIEW_SPECS: ClassVar[list[CurrentMetadataViewSpec]] = [ + CURRENT_METADATA_VIEW_SPEC, + CURRENT_RUN_METADATA_VIEW_SPEC, + ] diff --git a/timdex_dataset_api/metadata.py b/timdex_dataset_api/metadata.py index 11da914..b237965 100644 --- a/timdex_dataset_api/metadata.py +++ b/timdex_dataset_api/metadata.py @@ -4,10 +4,14 @@ import shutil import tempfile import time +from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Literal, Unpack, cast +from typing import TYPE_CHECKING, Any, ClassVar, Unpack, cast +from duckdb import BinderException as DuckDBBinderException +from duckdb import CatalogException as DuckDBCatalogException from duckdb import DuckDBPyConnection +from duckdb import IOException as DuckDBIOException from duckdb_engine import Dialect as DuckDBDialect from sqlalchemy import func, literal, select, text, tuple_ @@ -19,24 +23,65 @@ ) if TYPE_CHECKING: - from timdex_dataset_api.dataset import DatasetFilters, TIMDEXDataset + from timdex_dataset_api.dataset import TIMDEXDataset + from timdex_dataset_api.records import RecordsFilters logger = configure_logger(__name__) -ORDERED_METADATA_COLUMN_NAMES = [ - "timdex_record_id", - "source", - "run_date", - "run_type", - "action", - "run_id", - "run_record_offset", - "run_timestamp", - "filename", -] + +@dataclass(frozen=True) +class DataTypeMetadataConfig: + """Configuration for a data type's participation in the metadata layer.""" + + name: str + """Identifier and static DB table name, e.g. 'records', 'embeddings'.""" + + metadata_columns: list[str] + """Ordered column names for the static DB table and append deltas. + These are the lightweight metadata columns — no large payloads + (no source_record, transformed_record, embedding_vector, etc.).""" + + data_path: str + """Location of data parquet files, e.g. "data/records.""" + + prejoin_records: bool = True + """If True, metadata union views pre-join to metadata.records, adding + source, run_date, run_type, action, run_timestamp as columns. + Set to False for 'records' (columns are native); True for bolt-on types.""" + + +@dataclass(frozen=True) +class CurrentMetadataViewSpec: + """Domain-owned definition for a current-metadata view.""" + + name: str + """View name created in metadata schema, e.g. 'current_records'.""" + + query_sql: str + """SQL query body used to create the view.""" + + required_metadata_tables: list[str] + """Metadata tables/views that must exist before creating this view.""" + + preload_setting_attribute: str | None = None + """Optional TIMDEXDataset bool attribute controlling temp-table preload.""" class TIMDEXDatasetMetadata: + """Class to handle metadata for all data types in the TIMDEXDataset.""" + + BASE_METADATA_COLUMNS: ClassVar[list[str]] = [ + "timdex_record_id", + "source", + "run_date", + "run_type", + "action", + "run_id", + "run_record_offset", + "run_timestamp", + "filename", + ] + def __init__(self, timdex_dataset: "TIMDEXDataset") -> None: """Init TIMDEXDatasetMetadata. @@ -44,30 +89,15 @@ def __init__(self, timdex_dataset: "TIMDEXDataset") -> None: timdex_dataset: parent TIMDEXDataset instance """ self.timdex_dataset = timdex_dataset - self.conn = timdex_dataset.conn + self.data_type_configs = timdex_dataset.data_type_configs + self.current_metadata_view_specs = timdex_dataset.current_metadata_view_specs self.create_metadata_structure() self._setup_metadata_schema() - @property - def location(self) -> str: - return self.timdex_dataset.location - - @property - def location_scheme(self) -> Literal["file", "s3"]: - return self.timdex_dataset.location_scheme - - @property - def config(self) -> "TIMDEXDataset.config": # type: ignore[name-defined] - return self.timdex_dataset.config - - @property - def preload_current_records(self) -> bool: - return self.timdex_dataset.preload_current_records - @property def metadata_root(self) -> str: - return f"{self.location.removesuffix('/')}/metadata" + return f"{self.timdex_dataset.location.removesuffix('/')}/metadata" @property def metadata_database_filename(self) -> str: @@ -77,46 +107,106 @@ def metadata_database_filename(self) -> str: def metadata_database_path(self) -> str: return f"{self.metadata_root}/{self.metadata_database_filename}" - @property - def append_deltas_path(self) -> str: - return f"{self.metadata_root}/append_deltas" + def append_deltas_path_for(self, config: DataTypeMetadataConfig) -> str: + """Return the append deltas path for a specific data type.""" + return f"{self.metadata_root}/append_deltas/{config.name}" + + def get_config(self, name: str) -> DataTypeMetadataConfig: + """Lookup a DataTypeMetadataConfig by name.""" + for config in self.data_type_configs: + if config.name == name: + return config + raise ValueError(f"No metadata config for data type: {name}") + + def resolve_data_type_name_for_table(self, table: str) -> str: + """Resolve a metadata table/view name to its owning data type config name.""" + for config in self.data_type_configs: + if table == config.name or table.endswith(f"_{config.name}"): + return config.name + + raise ValueError(f"Could not resolve data type for metadata table '{table}'.") + + def data_type_metadata_columns_for(self, data_type_name: str) -> list[str]: + """Return type-specific metadata columns (excluding shared base metadata).""" + config = self.get_config(data_type_name) + return [ + column_name + for column_name in config.metadata_columns + if column_name not in self.BASE_METADATA_COLUMNS + ] + + def get_metadata_columns_for_table(self, table: str) -> list[str]: + """Return canonical metadata columns projected by read keyset queries. + + This method combines self.BASE_METADATA_COLUMNS with metadata columns specific to, + and identified by, the data class. + """ + sa_table = self.timdex_dataset.get_sa_table("metadata", table) + available_columns = set(sa_table.c.keys()) + + data_type_name = self.resolve_data_type_name_for_table(table) + type_metadata_columns = self.data_type_metadata_columns_for(data_type_name) + + expected_columns = self.BASE_METADATA_COLUMNS + type_metadata_columns + + projected_columns: list[str] = [] + for column_name in expected_columns: + if column_name not in available_columns: + continue + if column_name in projected_columns: + continue + projected_columns.append(column_name) + + return projected_columns @property def records_count(self) -> int: """Count of all records in dataset.""" - return self.conn.query(""" + return self.timdex_dataset.conn.query(""" select count(*) from metadata.records; """).fetchone()[0] # type: ignore[index] @property def current_records_count(self) -> int: """Count of all current records in dataset.""" - return self.conn.query(""" + return self.timdex_dataset.conn.query(""" select count(*) from metadata.current_records; """).fetchone()[0] # type: ignore[index] + def append_deltas_count_for(self, config: DataTypeMetadataConfig) -> int: + """Count append deltas rows for a single data type.""" + view_name = f"{config.name}_append_deltas" + return self.timdex_dataset.conn.query(f""" + select count(*) from metadata.{view_name}; + """).fetchone()[0] # type: ignore[index] + @property def append_deltas_count(self) -> int: - """Count of all append deltas.""" - return self.conn.query(""" - select count(*) from metadata.append_deltas; - """).fetchone()[0] # type: ignore[index] + """Count of append deltas rows across all registered data types.""" + total = 0 + for config in self.data_type_configs: + try: + total += self.append_deltas_count_for(config) + except (DuckDBCatalogException, DuckDBBinderException): + continue + return total def create_metadata_structure(self) -> None: """Ensure metadata structure exists in TIMDEX dataset.""" - if self.location_scheme == "file": + if self.timdex_dataset.location_scheme == "file": Path(self.metadata_database_path).parent.mkdir( parents=True, exist_ok=True, ) - Path(self.append_deltas_path).mkdir( - parents=True, - exist_ok=True, - ) + for config in self.data_type_configs: + Path(self.append_deltas_path_for(config)).mkdir( + parents=True, + exist_ok=True, + ) def database_exists(self) -> bool: """Check if static metadata database file exists.""" - if self.location_scheme == "s3": + if self.timdex_dataset.location_scheme == "s3": s3_client = S3Client() return s3_client.object_exists(self.metadata_database_path) return os.path.exists(self.metadata_database_path) @@ -130,22 +220,26 @@ def rebuild_dataset_metadata(self) -> None: - build a local, temporary static metadata database file, then overwrite the canonical version in the dataset (e.g. in S3) """ - if self.location_scheme == "s3": - s3_client = S3Client() - s3_client.delete_folder(self.append_deltas_path) - else: - shutil.rmtree(self.append_deltas_path, ignore_errors=True) + for config in self.data_type_configs: + deltas_path = self.append_deltas_path_for(config) + if self.timdex_dataset.location_scheme == "s3": + s3_client = S3Client() + s3_client.delete_folder(deltas_path) + else: + shutil.rmtree(deltas_path, ignore_errors=True) # build database locally with tempfile.TemporaryDirectory() as temp_dir: local_db_path = str(Path(temp_dir) / self.metadata_database_filename) - factory = DuckDBConnectionFactory(location_scheme=self.location_scheme) + factory = DuckDBConnectionFactory( + location_scheme=self.timdex_dataset.location_scheme + ) with factory.create_connection(local_db_path) as conn: self._create_full_dataset_table(conn) # copy local database file to remote location - if self.location_scheme == "s3": + if self.timdex_dataset.location_scheme == "s3": s3_client = S3Client() s3_client.upload_file( local_db_path, @@ -158,37 +252,45 @@ def rebuild_dataset_metadata(self) -> None: self.timdex_dataset.refresh() def _create_full_dataset_table(self, conn: DuckDBPyConnection) -> None: - """Create a table of metadata for all records in the ETL parquet dataset. + """Create metadata tables for all data types in the static database. - This is one of the few times we fully materialize data in a DuckDB connection. - This is most commonly used when recreating the baseline static metadata database - file. + Iterates over registered data type configs and creates one table per type. + Gracefully skips data types whose parquet data does not yet exist. """ - start_time = time.perf_counter() - logger.debug("creating table static_db.main.records") - - # temporarily increase thread count - conn.execute("""SET threads = 64;""") + for config in self.data_type_configs: + self._create_metadata_table(conn, config) - query = f""" - create or replace table records as ( - select - {",".join(ORDERED_METADATA_COLUMN_NAMES)} - from read_parquet( - '{self.location}/data/records/**/*.parquet', - hive_partitioning=true, - filename=true - ) - ); + def _create_metadata_table( + self, conn: DuckDBPyConnection, config: DataTypeMetadataConfig + ) -> None: + """Create a metadata table for a single data type in the static database.""" + start_time = time.perf_counter() + data_path = f"{self.timdex_dataset.location.removesuffix('/')}/{config.data_path}" + + logger.debug(f"creating table static_db.main.{config.name}") + + try: + sql_query = f""" + create or replace table {config.name} as ( + select {",".join(config.metadata_columns)} + from read_parquet( + '{data_path}/**/*.parquet', + hive_partitioning=true, + filename=true + ) + ); """ - conn.execute(query) - - # reset thread count - conn.execute(f"""SET threads = {self.timdex_dataset.conn_factory.threads};""") + conn.execute(sql_query) + except DuckDBIOException: + logger.warning( + f"Could not create metadata table for '{config.name}' " + f"(no parquet data at '{data_path}'). Skipping." + ) + return - row_count = conn.query("""select count(*) from records;""").fetchone()[0] # type: ignore[index] + row_count = conn.query(f"select count(*) from {config.name};").fetchone()[0] # type: ignore[index] logger.info( - f"'records' table created - rows: {row_count}, " + f"'{config.name}' table created - rows: {row_count}, " f"elapsed: {time.perf_counter() - start_time}" ) @@ -207,10 +309,14 @@ def _setup_metadata_schema(self) -> None: ) return - self._attach_database_file(self.conn) - self._create_append_deltas_view(self.conn) - self._create_records_union_view(self.conn) - self._create_current_records_view(self.conn) + self._attach_database_file(self.timdex_dataset.conn) + + for config in self.data_type_configs: + self._create_append_deltas_view(self.timdex_dataset.conn, config) + self._create_union_view(self.timdex_dataset.conn, config) + + for spec in self.current_metadata_view_specs: + self._create_current_metadata_view(self.timdex_dataset.conn, spec) logger.debug( "Metadata schema setup for TIMDEXDatasetMetadata, " @@ -229,175 +335,258 @@ def _attach_database_file(self, conn: DuckDBPyConnection) -> None: f"""attach '{self.metadata_database_path}' AS static_db (READ_ONLY);""" ) - def _create_append_deltas_view(self, conn: DuckDBPyConnection) -> None: - """Create a view that projects over append delta parquet files. + def _create_append_deltas_view( + self, conn: DuckDBPyConnection, config: DataTypeMetadataConfig + ) -> None: + """Create a view that projects over append delta parquet files for a data type. + + If there are NO append deltas (e.g. after a rebuild or merge), we still create a + view by utilizing the schema from the static DB table but without any rows. This + allows downstream views to be built on top of this view. - If when run there are NO append deltas, which could be true immediately after a - metadata base create/recreate or append delta merge, we still create a view by - utilizing the schema from static_db.records but without any rows. This allows us - to build additional downstream views on top of *this* view. Also noting that a - call to .refresh() will recreate this view. + The view is named ``metadata.{config.name}_append_deltas``. """ - logger.debug("creating view metadata.append_deltas") + view_name = f"{config.name}_append_deltas" + deltas_path = self.append_deltas_path_for(config) + static_table = f"static_db.{config.name}" + + logger.debug(f"creating view metadata.{view_name}") # get current append delta count append_delta_count = conn.execute(f""" select count(*) as file_count - from glob('{self.append_deltas_path}/*.parquet') - """).fetchone()[0] # type: ignore[index] - logger.debug(f"{append_delta_count} append deltas found") + from glob('{deltas_path}/*.parquet') + """).fetchone()[0] # type: ignore[index] + logger.debug(f"{append_delta_count} append deltas found for '{config.name}'") - # if deltas, create view projecting over those parquet files + # if deltas exist, always create this view from parquet files if append_delta_count > 0: - query = f""" - create or replace view metadata.append_deltas as ( - select * - from read_parquet( - '{self.append_deltas_path}/*.parquet', - filename = 'append_delta_filename' - ) - ); - """ - - # if not, create a view that mirrors the structure of static_db.records - else: - query = """ - create or replace view metadata.append_deltas as ( - select * - from static_db.records - where 1 = 0 - );""" - - conn.execute(query) + conn.execute(f""" + create or replace view metadata.{view_name} as ( + select * + from read_parquet( + '{deltas_path}/*.parquet', + filename = 'append_delta_filename' + ) + ); + """) + return - def _create_records_union_view(self, conn: DuckDBPyConnection) -> None: - logger.debug("creating view metadata.records") + # no deltas: if static table exists, create zero-row mirror + table_exists = conn.execute(f""" + select count(*) from information_schema.tables + where table_catalog = 'static_db' + and table_name = '{config.name}' + """).fetchone()[0] # type: ignore[index] - conn.execute(f""" - create or replace view metadata.records as - ( - select - {",".join(ORDERED_METADATA_COLUMN_NAMES)} - from static_db.records - union all - select - {",".join(ORDERED_METADATA_COLUMN_NAMES)} - from metadata.append_deltas - ); + if table_exists: + conn.execute(f""" + create or replace view metadata.{view_name} as ( + select *, + null::varchar as append_delta_filename + from {static_table} + where 1 = 0 + ); """) + return - def _create_current_records_view(self, conn: DuckDBPyConnection) -> None: - """Create a view of current records. + # no static table and no deltas, so no view to create + logger.debug( + f"No static table or append deltas found for '{config.name}'; " + f"skipping append deltas view for '{config.name}'." + ) - This view builds on the table `records`. + # columns added to bolt-on types via pre-join to metadata.records + PREJOIN_RECORDS_COLUMNS: ClassVar[list[str]] = [ + "source", + "run_date", + "run_type", + "action", + "run_timestamp", + ] + + def _create_union_view( + self, conn: DuckDBPyConnection, config: DataTypeMetadataConfig + ) -> None: + """Create a union view combining static DB and append deltas for a data type. + + The view is named ``metadata.{config.name}`` and unions + ``static_db.{config.name}`` with ``metadata.{config.name}_append_deltas``. + + For bolt-on data types (``config.prejoin_records=True``), the view pre-joins + to ``metadata.records`` so that ``source``, ``run_date``, ``run_type``, + ``action``, and ``run_timestamp`` are available as filterable columns. + """ + view_name = config.name + static_table = f"static_db.{config.name}" + deltas_view = f"metadata.{config.name}_append_deltas" + columns = ",".join(config.metadata_columns) + + logger.debug(f"creating view metadata.{view_name}") + + static_table_exists = conn.execute(f""" + select count(*) from information_schema.tables + where table_catalog = 'static_db' + and table_name = '{config.name}' + """).fetchone()[0] # type: ignore[index] + + deltas_view_exists = conn.execute(f""" + select count(*) from information_schema.tables + where table_schema = 'metadata' + and table_name = '{config.name}_append_deltas' + and table_type = 'VIEW' + """).fetchone()[0] # type: ignore[index] + + # build the base union (or single-source) subquery + base_subquery = self._build_base_union_sql( + static_table, + deltas_view, + columns, + static_table_exists, + deltas_view_exists, + ) - This metadata view includes only the most current version of each record in the - dataset. With the metadata provided from this view, we can streamline data - retrievals in TIMDEXDataset read methods. + if base_subquery is None: + logger.debug( + f"No static table or append deltas view found for '{config.name}'; " + f"skipping union view for '{config.name}'." + ) + return - By default, creates a view only (lazy evaluation). If - preload_current_records=True, creates a temp table for better performance - for repeated queries. + if config.prejoin_records: + prejoin_cols = ",".join(f"r.{c}" for c in self.PREJOIN_RECORDS_COLUMNS) + join_keys = "timdex_record_id, run_id, run_record_offset" + conn.execute(f""" + create or replace view metadata.{view_name} as + select + e.*, + {prejoin_cols} + from ({base_subquery}) e + join metadata.records r using ({join_keys}); + """) + else: + conn.execute(f""" + create or replace view metadata.{view_name} as + {base_subquery}; + """) - For temp table mode, the data is mostly in memory but has the ability to spill to - disk if we risk getting too close to our memory constraints. We explicitly set the - temporary location on disk for DuckDB at "/tmp" to play nice with contexts like - AWS ECS or Lambda, where sometimes the $HOME env var is missing; DuckDB often - tries to utilize the user's home directory and this works around that. - """ - logger.debug("creating view metadata.current_records") - - # SQL for the current records logic (CTEs) - current_records_query = """ - with - -- CTE of run_timestamp for last source full run - cr_source_last_full as ( - select - source, - max(run_timestamp) as last_full_ts - from metadata.records - where run_type = 'full' - group by source - ), - - -- CTE of all records, per source, on or after last full run - cr_since_last_full as ( - select - r.* - from metadata.records r - join cr_source_last_full f using (source) - where r.run_timestamp >= f.last_full_ts - ), - - -- CTE of records ranked by run_timestamp - cr_ranked_records as ( - select - r.*, - row_number() over ( - partition by r.source, r.timdex_record_id - order by - r.run_timestamp desc nulls last, - r.run_id desc nulls last, - r.run_record_offset desc nulls last - ) as rn - from cr_since_last_full r - ) + @staticmethod + def _build_base_union_sql( + static_table: str, + deltas_view: str, + columns: str, + static_table_exists: int, + deltas_view_exists: int, + ) -> str | None: + """Return base union SQL or None if neither source exists.""" + if static_table_exists and deltas_view_exists: + return f""" + select {columns} from {static_table} + union all + select {columns} from {deltas_view} + """ + if static_table_exists: + return f"select {columns} from {static_table}" + if deltas_view_exists: + return f"select {columns} from {deltas_view}" + return None + + def _create_current_metadata_view( + self, conn: DuckDBPyConnection, spec: CurrentMetadataViewSpec + ) -> None: + """Create a current metadata view from a registered domain-owned spec.""" + missing_tables = [ + table_name + for table_name in spec.required_metadata_tables + if not self._metadata_table_exists(conn, table_name) + ] + + if missing_tables: + logger.warning( + f"Skipping metadata.{spec.name} view creation because missing " + f"dependencies: {', '.join(missing_tables)}" + ) + return - -- final select for current records (rn = 1) - select - * exclude (rn) - from cr_ranked_records - where rn = 1 - """ + logger.debug(f"creating view metadata.{spec.name}") - # create temp table (materializes in memory) - if self.preload_current_records: - logger.debug("creating temp table temp.main.current_records") + if self._should_preload_current_view(spec): + logger.debug(f"creating temp table temp.main.{spec.name}") conn.execute("set temp_directory = '/tmp';") conn.execute(f""" - create or replace temp table temp.main.current_records as - {current_records_query}; + create or replace temp table temp.main.{spec.name} as + {spec.query_sql}; - -- create view in metadata schema that points to temp table - create or replace view metadata.current_records as - select * from temp.main.current_records; - """) + create or replace view metadata.{spec.name} as + select * from temp.main.{spec.name}; + """) + return - # create view only (lazy evaluation) - else: - conn.execute(f""" - create or replace view metadata.current_records as - {current_records_query}; - """) + conn.execute(f""" + create or replace view metadata.{spec.name} as + {spec.query_sql}; + """) + + def _should_preload_current_view(self, spec: CurrentMetadataViewSpec) -> bool: + """Return True when a view spec is configured for temp-table preloading.""" + if spec.preload_setting_attribute is None: + return False + return bool(getattr(self.timdex_dataset, spec.preload_setting_attribute, False)) + + def _metadata_table_exists(self, conn: DuckDBPyConnection, table_name: str) -> bool: + """Return True if a metadata schema table or view exists by name.""" + table_exists = conn.execute(f""" + select count(*) from information_schema.tables + where table_schema = 'metadata' + and table_name = '{table_name}' + """).fetchone()[0] # type: ignore[index] + return bool(table_exists) def merge_append_deltas(self) -> None: - """Merge append deltas into the static metadata database file.""" + """Merge append deltas into the static metadata database file. + + Iterates over all data type configs, merging each type's deltas into its + corresponding table in the static database. + """ logger.info("merging append deltas into static metadata database file") start_time = time.perf_counter() s3_client = S3Client() - # get filenames of append deltas - append_delta_filenames = ( - self.conn.query(""" - select distinct(append_delta_filename) - from metadata.append_deltas - """) - .to_df()["append_delta_filename"] - .to_list() - ) - - if len(append_delta_filenames) == 0: + # collect all append delta filenames across all types + all_delta_filenames: dict[str, list[str]] = {} + has_any_deltas = False + for config in self.data_type_configs: + deltas_view = f"{config.name}_append_deltas" + try: + filenames = ( + self.timdex_dataset.conn.query(f""" + select distinct(append_delta_filename) + from metadata.{deltas_view} + """) + .to_df()["append_delta_filename"] + .to_list() + ) + except ( + DuckDBIOException, + DuckDBCatalogException, + DuckDBBinderException, + KeyError, + ): + filenames = [] + all_delta_filenames[config.name] = filenames + if filenames: + has_any_deltas = True + + if not has_any_deltas: logger.info("no append deltas found") return - logger.debug(f"{len(append_delta_filenames)} append deltas found") - with tempfile.TemporaryDirectory() as temp_dir: # create local copy of the static metadata database (static db) file local_db_path = str(Path(temp_dir) / self.metadata_database_filename) - if self.location_scheme == "s3": + if self.timdex_dataset.location_scheme == "s3": s3_client.download_file( s3_uri=self.metadata_database_path, local_path=local_db_path ) @@ -405,21 +594,21 @@ def merge_append_deltas(self) -> None: shutil.copy(src=self.metadata_database_path, dst=local_db_path) # attach to local static db - self.conn.execute(f"""attach '{local_db_path}' AS local_static_db;""") + self.timdex_dataset.conn.execute( + f"""attach '{local_db_path}' AS local_static_db;""" + ) - # insert records from append deltas to local static db - self.conn.execute(f""" - insert into local_static_db.records - select - {",".join(ORDERED_METADATA_COLUMN_NAMES)} - from metadata.append_deltas - """) + # merge deltas for each data type + for config in self.data_type_configs: + if not all_delta_filenames[config.name]: + continue + self._merge_deltas_for_type(config) # detach from local static db - self.conn.execute("""detach local_static_db;""") + self.timdex_dataset.conn.execute("""detach local_static_db;""") # overwrite static db file with local version - if self.location_scheme == "s3": + if self.timdex_dataset.location_scheme == "s3": s3_client.upload_file( local_db_path, self.metadata_database_path, @@ -427,40 +616,77 @@ def merge_append_deltas(self) -> None: else: shutil.copy(src=local_db_path, dst=self.metadata_database_path) - # delete append deltas - for append_delta_filename in append_delta_filenames: - if self.location_scheme == "s3": - s3_client.delete_file(s3_uri=append_delta_filename) - else: - os.remove(append_delta_filename) + # delete append deltas for all types + for config in self.data_type_configs: + for delta_filename in all_delta_filenames[config.name]: + if self.timdex_dataset.location_scheme == "s3": + s3_client.delete_file(s3_uri=delta_filename) + else: + os.remove(delta_filename) logger.debug( "append deltas merged into the static metadata database file: " f"{self.metadata_database_path}, {time.perf_counter() - start_time}s" ) - def write_append_delta_duckdb(self, filepath: str) -> None: - """Write an append delta for an ETL parquet file. + def _merge_deltas_for_type(self, config: DataTypeMetadataConfig) -> None: + """Insert rows from append deltas into the local static DB for one data type.""" + columns = ",".join(config.metadata_columns) + deltas_view = f"metadata.{config.name}_append_deltas" + + logger.debug(f"merging append deltas for '{config.name}'") + + # if type table doesn't yet exist in static DB, initialize it from deltas schema + table_exists = self.timdex_dataset.conn.execute(f""" + select count(*) from information_schema.tables + where table_catalog = 'local_static_db' + and table_name = '{config.name}' + """).fetchone()[0] # type: ignore[index] + + if not table_exists: + self.timdex_dataset.conn.execute(f""" + create table local_static_db.{config.name} as + select {columns} + from {deltas_view} + where 1 = 0 + """) - A DuckDB context is used to both read metadata-only columns from the ETL parquet - file, then write an append delta parquet file to /metadata/append_deltas. The - write is performed by DuckDB's COPY function. + self.timdex_dataset.conn.execute(f""" + insert into local_static_db.{config.name} + select {columns} + from {deltas_view} + """) + + def write_append_delta( + self, + filepath: str, + config: DataTypeMetadataConfig, + ) -> None: + """Write an append delta for a parquet file. + + A DuckDB context is used to read metadata-only columns from the parquet + file, then write an append delta parquet file to + ``metadata/append_deltas/{config.name}/``. Note: this operation is safe in parallel with other possible append delta writes. + + Args: + filepath: path to the parquet file to extract metadata from + config: the DataTypeMetadataConfig for this data type """ start_time = time.perf_counter() - output_path = f"{self.append_deltas_path}/append_delta-{filepath.split('/')[-1]}" + deltas_path = self.append_deltas_path_for(config) + output_path = f"{deltas_path}/append_delta-{filepath.split('/')[-1]}" # ensure s3:// schema prefix is present - if self.location_scheme == "s3": + if self.timdex_dataset.location_scheme == "s3": filepath = f"s3://{filepath.removeprefix('s3://')}" - # perform query + write as one SQL statement sql = f""" copy ( select - {",".join(ORDERED_METADATA_COLUMN_NAMES)} + {",".join(config.metadata_columns)} from read_parquet( '{filepath}', hive_partitioning=true, @@ -469,7 +695,7 @@ def write_append_delta_duckdb(self, filepath: str) -> None: ) to '{output_path}' (FORMAT parquet); """ - self.conn.execute(sql) + self.timdex_dataset.conn.execute(sql) logger.debug( f"Append delta written: {output_path}, {time.perf_counter() - start_time}s" @@ -482,20 +708,42 @@ def build_keyset_paginated_metadata_query( limit: int | None = None, where: str | None = None, keyset_value: tuple[int, int, int] = (0, 0, 0), - **filters: Unpack["DatasetFilters"], + **filters: Unpack["RecordsFilters"], ) -> str: - """Build SQL query using SQLAlchemy against metadata schema tables and views.""" + """Build SQL query using SQLAlchemy against metadata schema tables and views. + + Args: + table: metadata table/view name + limit: max rows to return + where: raw SQL WHERE clause + keyset_value: tuple of (filename_hash, run_id_hash, run_record_offset) + for keyset pagination + **filters: key/value filter pairs + """ sa_table = self.timdex_dataset.get_sa_table("metadata", table) + required_keyset_columns = {"filename", "run_id", "run_record_offset"} + missing_keyset_columns = required_keyset_columns - set(sa_table.c.keys()) + if missing_keyset_columns: + missing = ", ".join(sorted(missing_keyset_columns)) + raise ValueError( + f"Table '{table}' missing required keyset column(s): {missing}" + ) + + metadata_columns = self.get_metadata_columns_for_table(table) + # create SQL statement object - stmt = select( - sa_table.c.timdex_record_id, - sa_table.c.run_id, - func.hash(sa_table.c.run_id).label("run_id_hash"), - sa_table.c.run_record_offset, - sa_table.c.filename, - func.hash(sa_table.c.filename).label("filename_hash"), - ).select_from(sa_table) + select_columns: list[Any] = [ + sa_table.c[column_name] for column_name in metadata_columns + ] + select_columns.extend( + [ + func.hash(sa_table.c.run_id).label("run_id_hash"), + func.hash(sa_table.c.filename).label("filename_hash"), + ] + ) + + stmt = select(*select_columns).select_from(sa_table) # filter expressions from key/value filters (may return None) filter_expr = build_filter_expr_sa(sa_table, **cast("dict", filters)) @@ -507,7 +755,7 @@ def build_keyset_paginated_metadata_query( stmt = stmt.where(text(where)) # keyset pagination - filename_has, run_id_hash, run_record_offset_ = keyset_value + filename_hash, run_id_hash, run_record_offset_ = keyset_value stmt = stmt.where( tuple_( func.hash(sa_table.c.filename), @@ -515,7 +763,7 @@ def build_keyset_paginated_metadata_query( sa_table.c.run_record_offset, ) > tuple_( - literal(filename_has), + literal(filename_hash), literal(run_id_hash), literal(run_record_offset_), ) diff --git a/timdex_dataset_api/record.py b/timdex_dataset_api/record.py deleted file mode 100644 index 21a62c4..0000000 --- a/timdex_dataset_api/record.py +++ /dev/null @@ -1,69 +0,0 @@ -"""timdex_dataset_api/record.py""" - -from datetime import UTC, date, datetime - -import attrs -from attrs import asdict, define, field - - -def strict_date_parse(date_string: str) -> date: - return datetime.strptime(date_string, "%Y-%m-%d").astimezone(UTC).date() - - -def datetime_iso_parse(datetime_iso_string: str) -> datetime: - parsed_datetime = datetime.fromisoformat(datetime_iso_string) - # if timezone not present, set as UTC and return - if parsed_datetime.tzinfo is None: - return parsed_datetime.replace(tzinfo=UTC) - # else, convert to / ensure UTC and return - return parsed_datetime.astimezone(UTC) - - -@define -class DatasetRecord: - """Container for single dataset record. - - An iterator of these are passed to the TIMDEXDataset.write() method, where they are - first serialized into dictionaries, and then grouped into pyarrow.RecordBatches for - writing. - """ - - timdex_record_id: str = field() - source_record: bytes = field() - transformed_record: bytes = field() - source: str = field() - run_date: date = field(converter=strict_date_parse) - run_type: str = field() - action: str = field() - run_id: str = field() - run_timestamp: datetime = field( - converter=datetime_iso_parse, - default=attrs.Factory( - lambda self: self.run_date.isoformat(), - takes_self=True, - ), - ) - run_record_offset: int = field(default=None) - - @property - def year(self) -> str: - return self.run_date.strftime("%Y") - - @property - def month(self) -> str: - return self.run_date.strftime("%m") - - @property - def day(self) -> str: - return self.run_date.strftime("%d") - - def to_dict( - self, - ) -> dict: - """Serialize instance as dictionary.""" - return { - **asdict(self), - "year": self.year, - "month": self.month, - "day": self.day, - } diff --git a/timdex_dataset_api/records.py b/timdex_dataset_api/records.py new file mode 100644 index 0000000..8d68810 --- /dev/null +++ b/timdex_dataset_api/records.py @@ -0,0 +1,199 @@ +"""timdex_dataset_api/records.py""" + +import json +from collections.abc import Iterator +from datetime import date, datetime +from typing import ClassVar, TypedDict, Unpack + +import attrs +import pyarrow as pa +from attrs import asdict, define, field + +from timdex_dataset_api.data_source import TIMDEXDataSource, ValidTable +from timdex_dataset_api.metadata import CurrentMetadataViewSpec +from timdex_dataset_api.utils import ( + datetime_iso_parse, + strict_date_parse, +) + + +class RecordsFilters(TypedDict, total=False): + timdex_record_id: str | list[str] | None + source: str | list[str] | None + run_date: str | date | list[str | date] | None + run_type: str | list[str] | None + action: str | list[str] | None + run_id: str | list[str] | None + run_record_offset: int | list[int] | None + run_timestamp: str | datetime | list[str | datetime] | None + + +@define +class DatasetRecord: + """Container for single dataset record. + + An iterator of these are passed to the TIMDEXRecords.write() method, where they are + first serialized into dictionaries, and then grouped into pyarrow.RecordBatches for + writing. + """ + + timdex_record_id: str = field() + source_record: bytes = field() + transformed_record: bytes = field() + source: str = field() + run_date: date = field(converter=strict_date_parse) + run_type: str = field() + action: str = field() + run_id: str = field() + run_timestamp: datetime = field( + converter=datetime_iso_parse, + default=attrs.Factory( + lambda self: self.run_date.isoformat(), + takes_self=True, + ), + ) + run_record_offset: int = field(default=None) + + @property + def year(self) -> str: + return self.run_date.strftime("%Y") + + @property + def month(self) -> str: + return self.run_date.strftime("%m") + + @property + def day(self) -> str: + return self.run_date.strftime("%d") + + def to_dict( + self, + ) -> dict: + """Serialize instance as dictionary.""" + return { + **asdict(self), + "year": self.year, + "month": self.month, + "day": self.day, + } + + +class TIMDEXRecords(TIMDEXDataSource): + """Class to handle records in the TIMDEXDataset.""" + + NAME: ClassVar[str] = "records" + + SCHEMA: ClassVar[pa.Schema] = pa.schema( + ( + pa.field("timdex_record_id", pa.string()), + pa.field("source_record", pa.binary()), + pa.field("transformed_record", pa.binary()), + pa.field("source", pa.string()), + pa.field("run_date", pa.date32()), + pa.field("run_type", pa.string()), + pa.field("action", pa.string()), + pa.field("run_id", pa.string()), + pa.field("run_record_offset", pa.int32()), + pa.field("year", pa.string()), + pa.field("month", pa.string()), + pa.field("day", pa.string()), + pa.field("run_timestamp", pa.timestamp("us", tz="UTC")), + ) + ) + + DATA_COLUMNS: ClassVar[list[str]] = [ + "source_record", + "transformed_record", + ] + + DATA_PATH: ClassVar[str] = "data/records" + + PREJOIN_RECORDS: ClassVar[bool] = False + + VALID_TABLES: ClassVar[list[ValidTable]] = [ + ValidTable( + name="records", + description="All record versions across all runs.", + ), + ValidTable( + name="current_records", + description=( + "One row per (source, timdex_record_id) representing the" + " most recent version of each record since the last full" + " run." + ), + ), + ] + + CURRENT_METADATA_VIEW_QUERY: ClassVar[str] = """ + with + -- CTE of run_timestamp for last source full run + cr_source_last_full as ( + select + source, + max(run_timestamp) as last_full_ts + from metadata.records + where run_type = 'full' + group by source + ), + + -- CTE of all records, per source, on or after last full run + cr_since_last_full as ( + select + r.* + from metadata.records r + join cr_source_last_full f using (source) + where r.run_timestamp >= f.last_full_ts + ), + + -- CTE of records ranked by run_timestamp + cr_ranked_records as ( + select + r.*, + row_number() over ( + partition by r.source, r.timdex_record_id + order by + r.run_timestamp desc nulls last, + r.run_id desc nulls last, + r.run_record_offset desc nulls last + ) as rn + from cr_since_last_full r + ) + + -- final select for current records (rn = 1) + select + * exclude (rn) + from cr_ranked_records + where rn = 1 + """ + + CURRENT_METADATA_VIEW_SPEC: ClassVar[CurrentMetadataViewSpec] = ( + CurrentMetadataViewSpec( + name="current_records", + query_sql=CURRENT_METADATA_VIEW_QUERY, + required_metadata_tables=["records"], + preload_setting_attribute="preload_current_records", + ) + ) + + CURRENT_VIEW_SPECS: ClassVar[list[CurrentMetadataViewSpec]] = [ + CURRENT_METADATA_VIEW_SPEC + ] + + def read_transformed_records_iter( + self, + table: str = "records", + limit: int | None = None, + where: str | None = None, + **filters: Unpack[RecordsFilters], + ) -> Iterator[dict]: + """Custom read method to yield parsed transformed TIMDEX JSON records only.""" + for record_dict in self.read_dicts_iter( + table=table, + columns=["transformed_record"], + limit=limit, + where=where, + **filters, + ): + if transformed_record := record_dict["transformed_record"]: + yield json.loads(transformed_record) diff --git a/timdex_dataset_api/utils.py b/timdex_dataset_api/utils.py index 0d7c16e..627fb8d 100644 --- a/timdex_dataset_api/utils.py +++ b/timdex_dataset_api/utils.py @@ -289,3 +289,14 @@ def build_filter_expr_sa( if predicates: return and_(*predicates) return None + + +def strict_date_parse(date_string: str) -> date: + return datetime.strptime(date_string, "%Y-%m-%d").astimezone(UTC).date() + + +def datetime_iso_parse(datetime_iso_string: str) -> datetime: + parsed_datetime = datetime.fromisoformat(datetime_iso_string) + if parsed_datetime.tzinfo is None: + return parsed_datetime.replace(tzinfo=UTC) + return parsed_datetime.astimezone(UTC) From 631c50090a23ef63d13bcdd606f88a11aed1cde7 Mon Sep 17 00:00:00 2001 From: Graham Hukill Date: Tue, 7 Apr 2026 16:23:44 -0400 Subject: [PATCH 3/6] Cleanup and documentation updates Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/USE-496 --- README.md | 40 +++++----- docs/reading.md | 74 ++++++++++++------- ...025_05_30_backfill_run_timestamp_column.py | 5 +- ...25_consistent_run_timestamp_per_etl_run.py | 5 +- 4 files changed, 72 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 844f37f..9eb1bfb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -from timdex_dataset_api import TIMDEXDataset - # timdex-dataset-api Python library for interacting with a TIMDEX parquet dataset located remotely or in S3. This library is often abbreviated as "TDA". @@ -11,9 +9,10 @@ Python library for interacting with a TIMDEX parquet dataset located remotely or - To run unit tests: `make test` - To lint the repo: `make lint` -The library version number is set in [`timdex_dataset_api/__init__.py`](timdex_dataset_api/__init__.py), e.g.: -```python -__version__ = "2.1.0" +The library version number is set in [`pyproject.toml`](pyproject.toml), e.g.: +```toml +[project] +version = "5.0.0" ``` Updating the version number when making changes to the library will prompt applications that install it, when they have _their_ dependencies updated, to pickup the new version. @@ -74,15 +73,6 @@ With the env var `MINIO_S3_ENDPOINT_URL` set, this library will configure `pyarr ## Usage -Currently, the most common use cases are: - * **Transmogrifier**: uses TDA to **write** to the parquet dataset - * **TIMDEX-Index-Manager (TIM)**: uses TDA to **read** from the parquet dataset - -Beyond those two ETL run use cases, others are emerging where this library proves helpful: - - * yielding only the current version of all records in the dataset, useful for quickly re-indexing to Opensearch - * high throughput (time) + memory safe (space) access to the dataset for analysis - For both reading and writing, the following env vars are recommended: ```shell TDA_LOG_LEVEL=INFO @@ -105,15 +95,19 @@ timdex_dataset = TIMDEXDataset("s3://my-bucket/path/to/dataset") timdex_dataset = TIMDEXDataset("/path/to/dataset") ``` -All read methods for `TIMDEXDataset` allow for the same group of filters which are defined in `timdex_dataset_api.dataset.DatasetFilters`. Examples are shown below. +Source-specific operations are available on composed objects such as +`timdex_dataset.records` and `timdex_dataset.embeddings`. + +All read methods for `timdex_dataset.records` allow for the same group of filters. +Examples are shown below. ```python -# read a single row, no filtering -single_record_dict = next(timdex_dataset.read_dicts_iter()) +# read a single record row, no filtering +single_record_dict = next(timdex_dataset.records.read_dicts_iter()) # get batches of records, filtering to a particular run -for batch in timdex_dataset.read_batches_iter( +for batch in timdex_dataset.records.read_batches_iter( source="alma", run_date="2025-06-01", run_id="abc123" @@ -123,7 +117,7 @@ for batch in timdex_dataset.read_batches_iter( # use convenience method to yield only transformed records # NOTE: this is what TIM uses for indexing to Opensearch for a given ETL run -for transformed_record in timdex_dataset.read_transformed_records_iter( +for transformed_record in timdex_dataset.records.read_transformed_records_iter( source="aspace", run_date="2025-06-01", run_id="ghi789" @@ -133,7 +127,7 @@ for transformed_record in timdex_dataset.read_transformed_records_iter( # load all records for a given run into a pandas dataframe # NOTE: this can be potentially expensive memory-wise if the run is large -run_df = timdex_dataset.read_dataframe( +run_df = timdex_dataset.records.read_dataframe( source="dspace", run_date="2025-06-01", run_id="def456" @@ -146,7 +140,9 @@ See [docs/reading.md](docs/reading.md) for more information. At this time, the only application that writes to the ETL parquet dataset is Transmogrifier. -To write records to the dataset, you must prepare an iterator of `timdex_dataset_api.record.DatasetRecord`. Here is some pseudocode for how a dataset write can work: +To write records to the dataset, you must prepare an iterator of +`timdex_dataset_api.records.DatasetRecord`. Here is some pseudocode for how a +record dataset write can work: ```python from timdex_dataset_api import DatasetRecord, TIMDEXDataset @@ -171,5 +167,5 @@ records_iter = records_to_write_iter() # finally, perform the write, relying on the library to handle efficient batching timdex_dataset = TIMDEXDataset("/path/to/dataset") -timdex_dataset.write(records_iter=records_iter) +timdex_dataset.records.write(records_iter) ``` \ No newline at end of file diff --git a/docs/reading.md b/docs/reading.md index f45f52f..0ac523b 100644 --- a/docs/reading.md +++ b/docs/reading.md @@ -1,28 +1,35 @@ # Reading data from TIMDEXDataset -This guide explains how `TIMDEXDataset` read methods work and how to use them effectively. +This guide explains how TIMDEXDataset data source read methods work and how to use them effectively. -- `TIMDEXDataset` and `TIMDEXDatasetMetadata` both maintain an in-memory DuckDB context. You can issue DuckDB SQL against the views/tables they create. +- `TIMDEXDataset` maintains an in-memory DuckDB context. You can issue DuckDB SQL against the views/tables they create. +- Source-specific read methods are exposed on `timdex_dataset.records` and `timdex_dataset.embeddings`. - Read methods use a two-step query flow for performance: 1) a metadata query determines which Parquet files and row offsets are relevant 2) a data query reads just those rows and returns the requested columns -- Prefer simple key/value `DatasetFilters` for most use cases; add a `where=` SQL predicate when you need more advanced logic (e.g., ranges, `BETWEEN`, `>`, `<`, `IN`). +- Prefer simple key/value filters for most use cases; add a `where=` SQL predicate when you need more advanced logic (e.g., ranges, `BETWEEN`, `>`, `<`, `IN`). ## Available read methods +The shared read methods below are available on both `timdex_dataset.records` and +`timdex_dataset.embeddings`: + - `read_batches_iter(...)`: yields `pyarrow.RecordBatch` - `read_dicts_iter(...)`: yields Python `dict` per row - `read_dataframe(...)`: returns a pandas `DataFrame` - `read_dataframes_iter(...)`: yields pandas `DataFrame` batches + +Additionally, `timdex_dataset.records` provides: + - `read_transformed_records_iter(...)`: yields `transformed_record` dictionaries only -All accept the same `DatasetFilters` and the optional `where=` SQL predicate. +All accept the same key/value filters and the optional `where=` SQL predicate. ## Filters vs. where= -- `DatasetFilters` are key/value arguments on read methods. They are validated and translated into SQL and will cover most queries. +- Key/value filters are keyword arguments on read methods. They are validated and translated into SQL and will cover most queries. - Examples: `source="alma"`, `run_date="2024-12-01"`, `run_type="daily"`, `action="index"` -- `where=` is an optional raw SQL WHERE predicate string, combined with `DatasetFilters` using `AND`. Use it for: +- `where=` is an optional raw SQL WHERE predicate string, combined with these filters using `AND`. Use it for: - date/time ranges (BETWEEN, >, <) - set membership (IN (...)) - complex boolean logic (AND/OR grouping) @@ -46,7 +53,7 @@ This pattern keeps reads fast and memory-efficient even for large datasets. The following diagram shows the flow for an example query: ```python -for record_dict in td.read_dicts_iter( +for record_dict in td.records.read_dicts_iter( table="records", source="dspace", run_date="2025-09-01", @@ -65,7 +72,7 @@ sequenceDiagram participant P as Parquet files U->>TD: Perform query - Note left of TD: read_dicts_iter(
table="records",
source="dspace",
run_date="2025-09-01",
run_id="abc123") + Note left of TD: records.read_dicts_iter(
table="records",
source="dspace",
run_date="2025-09-01",
run_id="abc123") TD->>TDM: build_meta_query(table, filters, where=None) Note right of TDM: (Metadata Query)

SELECT r.timdex_record_id, r.run_id, r.filename, r.run_record_offset
FROM metadata.records r
WHERE r.source = 'dspace'
AND r.run_date = '2025-09-01'
AND r.run_id = 'abc123'
ORDER BY r.filename, r.run_record_offset @@ -88,66 +95,81 @@ from timdex_dataset_api import TIMDEXDataset td = TIMDEXDataset("s3://my-bucket/timdex-dataset") # example instance # 1) Get a single record as a dict -first = next(td.read_dicts_iter()) +first = next(td.records.read_dicts_iter()) # 2) Read batches with simple filters -for batch in td.read_batches_iter(source="alma", run_date="2025-06-01", run_id="abc123"): +for batch in td.records.read_batches_iter( + source="alma", + run_date="2025-06-01", + run_id="abc123", +): ... # process pyarrow.RecordBatch # 3) DataFrame of one run -df = td.read_dataframe(source="dspace", run_date="2025-06-01", run_id="def456") +df = td.records.read_dataframe( + source="dspace", + run_date="2025-06-01", + run_id="def456", +) # 4) Only transformed records (used by indexer) -for rec in td.read_transformed_records_iter(source="aspace", run_type="daily"): +for rec in td.records.read_transformed_records_iter( + source="aspace", + run_type="daily", +): ... # rec is a dict of the transformed_record ``` ## `where=` examples -Advanced filtering that complements `DatasetFilters`. +Advanced filtering that complements key/value filters. ```python # date range with BETWEEN where = "run_date BETWEEN '2024-12-01' AND '2024-12-31'" -df = td.read_dataframe(source="alma", where=where) +df = td.records.read_dataframe(source="alma", where=where) # greater-than on a timestamp (if present in columns) where = "run_timestamp > '2024-12-01T10:00:00Z'" -df = td.read_dataframe(source="aspace", run_type="daily", where=where) +df = td.records.read_dataframe(source="aspace", run_type="daily", where=where) # combine set membership and action where = "run_id IN ('run-1', 'run-3', 'run-5') AND action = 'index'" -df = td.read_dataframe(source="alma", where=where) +df = td.records.read_dataframe(source="alma", where=where) # combine filters (AND) with where= where = "run_type = 'daily' AND action = 'index'" -df = td.read_dataframe(source="libguides", where=where) +df = td.records.read_dataframe(source="libguides", where=where) ``` Validation tips: - Use only a predicate (no SELECT/FROM, no trailing semicolon). - Column names must exist in the target table/view (e.g., records or current_records). -- `DatasetFilters` + `where=` are ANDed; if the combination yields zero rows, you’ll get an empty result. +- Key/value filters + `where=` are ANDed; if the combination yields zero rows, you’ll get an empty result. ## Choosing a table -By default, read methods query the `records` view (all versions). To get only the latest version per `timdex_record_id`, target the `current_records` view: +For `timdex_dataset.records`, read methods query the `records` table by default (all versions). To get only the latest version per `timdex_record_id`, target the `current_records` view: ```python # ALL records in the 'libguides' source -all_libguides_df = td.read_dataframe(table="records", source="libguides") +all_libguides_df = td.records.read_dataframe(table="records", source="libguides") # latest unique records across the dataset -current_df = td.read_dataframe(table="current_records") +current_df = td.records.read_dataframe(table="current_records") # current records for a source and specific run -current_df = td.read_dataframe(table="current_records", source="alma", run_id="run-5") +current_df = td.records.read_dataframe( + table="current_records", + source="alma", + run_id="run-5", +) ``` ## DuckDB context -- `TIMDEXDataset` exposes a DuckDB connection used for data queries against Parquet. -- `TIMDEXDatasetMetadata` exposes a DuckDB connection used for metadata queries and provides views: +- `TIMDEXDataset` exposes a DuckDB connection used for metadata and data queries against Parquet. +- `TIMDEXDatasetMetadata` manages the metadata structures and provides views such as: - `metadata.records`: all record versions with run metadata - `metadata.current_records`: latest record per `timdex_record_id` - `metadata.append_deltas`: incremental write tracking @@ -155,8 +177,8 @@ current_df = td.read_dataframe(table="current_records", source="alma", run_id="r You can execute raw DuckDB SQL for inspection and debugging: ```python -# access metadata connection -conn = td.metadata.conn # DuckDB connection +# access dataset DuckDB connection +conn = td.conn # DuckDB connection # peek at view schemas print(conn.sql("DESCRIBE metadata.records").to_df()) diff --git a/migrations/001_2025_05_30_backfill_run_timestamp_column.py b/migrations/001_2025_05_30_backfill_run_timestamp_column.py index e952c87..f0fb9cf 100644 --- a/migrations/001_2025_05_30_backfill_run_timestamp_column.py +++ b/migrations/001_2025_05_30_backfill_run_timestamp_column.py @@ -41,7 +41,8 @@ from pyarrow import fs from timdex_dataset_api.config import configure_dev_logger, configure_logger -from timdex_dataset_api.dataset import TIMDEX_DATASET_SCHEMA, TIMDEXDataset +from timdex_dataset_api.dataset import TIMDEXDataset +from timdex_dataset_api.records import TIMDEXRecords configure_dev_logger() @@ -125,7 +126,7 @@ def backfill_parquet_file( # Create run_timestamp column using the exact schema definition num_rows = len(table) - run_timestamp_field = TIMDEX_DATASET_SCHEMA.field("run_timestamp") + run_timestamp_field = TIMDEXRecords.SCHEMA.field("run_timestamp") run_timestamp_array = pa.array( [creation_date] * num_rows, type=run_timestamp_field.type ) diff --git a/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py b/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py index 1014adb..e0c1247 100644 --- a/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py +++ b/migrations/002_2025_06_25_consistent_run_timestamp_per_etl_run.py @@ -50,7 +50,8 @@ from timdex_dataset_api import TIMDEXDatasetMetadata from timdex_dataset_api.config import configure_dev_logger, configure_logger -from timdex_dataset_api.dataset import TIMDEX_DATASET_SCHEMA, TIMDEXDataset +from timdex_dataset_api.dataset import TIMDEXDataset +from timdex_dataset_api.records import TIMDEXRecords configure_dev_logger() @@ -174,7 +175,7 @@ def backfill_parquet_file( # set new run_timestamp value num_rows = len(table) - run_timestamp_field = TIMDEX_DATASET_SCHEMA.field("run_timestamp") + run_timestamp_field = TIMDEXRecords.SCHEMA.field("run_timestamp") new_run_timestamp_array = pa.array( [new_run_timestamp] * num_rows, type=run_timestamp_field.type ) From 8eadf94f13da0887c892802624118947dcf28154 Mon Sep 17 00:00:00 2001 From: Graham Hukill Date: Wed, 8 Apr 2026 09:25:05 -0400 Subject: [PATCH 4/6] Re-add increased thread count for metadata rebuild --- timdex_dataset_api/metadata.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/timdex_dataset_api/metadata.py b/timdex_dataset_api/metadata.py index b237965..8db5c89 100644 --- a/timdex_dataset_api/metadata.py +++ b/timdex_dataset_api/metadata.py @@ -269,6 +269,9 @@ def _create_metadata_table( logger.debug(f"creating table static_db.main.{config.name}") + # temporarily increase thread count for parallel parquet file scanning + conn.execute("SET threads = 64;") + try: sql_query = f""" create or replace table {config.name} as ( @@ -288,6 +291,9 @@ def _create_metadata_table( ) return + # reset thread count + conn.execute(f"""SET threads = {self.timdex_dataset.conn_factory.threads};""") + row_count = conn.query(f"select count(*) from {config.name};").fetchone()[0] # type: ignore[index] logger.info( f"'{config.name}' table created - rows: {row_count}, " From 8382797465e2daae2e8153f306e5892e40254fc2 Mon Sep 17 00:00:00 2001 From: Graham Hukill Date: Thu, 9 Apr 2026 16:07:50 -0400 Subject: [PATCH 5/6] Refactor metadata around data source table configs Why these changes are being introduced: * Continues the data source refactor by removing the split between metadata type configs, valid tables, and current view specs. * The previous structure duplicated table metadata across the data sources and metadata layer, which made readable tables and derived metadata columns harder to maintain. How this addresses that need: * Introduce DataSourceTableConfig so each data source defines its base and custom readable tables in one place. * Derive metadata/read column lists directly from each source schema and use source classes throughout TIMDEXDatasetMetadata. * Update exports and tests to use the new source-driven table and column interfaces. Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/USE-496 --- docs/reading.md | 6 +- tests/test_embeddings.py | 6 +- tests/test_metadata.py | 129 ++++++------ tests/test_read.py | 2 +- tests/test_write.py | 8 +- timdex_dataset_api/__init__.py | 12 +- timdex_dataset_api/data_source.py | 164 ++++++---------- timdex_dataset_api/dataset.py | 17 +- timdex_dataset_api/embeddings.py | 68 +++---- timdex_dataset_api/metadata.py | 317 +++++++++++++----------------- timdex_dataset_api/records.py | 38 ++-- 11 files changed, 318 insertions(+), 449 deletions(-) diff --git a/docs/reading.md b/docs/reading.md index 0ac523b..e5597d3 100644 --- a/docs/reading.md +++ b/docs/reading.md @@ -169,10 +169,8 @@ current_df = td.records.read_dataframe( ## DuckDB context - `TIMDEXDataset` exposes a DuckDB connection used for metadata and data queries against Parquet. -- `TIMDEXDatasetMetadata` manages the metadata structures and provides views such as: - - `metadata.records`: all record versions with run metadata - - `metadata.current_records`: latest record per `timdex_record_id` - - `metadata.append_deltas`: incremental write tracking +- `TIMDEXDataSource` provides a base class that data sources extend + - each data source class defines "tables" that are available for that source in the `metadata` schema You can execute raw DuckDB SQL for inspection and debugging: diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index 9bcb149..6a74e4f 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -12,7 +12,7 @@ from timdex_dataset_api import TIMDEXDataset from timdex_dataset_api.embeddings import DatasetEmbedding, TIMDEXEmbeddings -EMBEDDINGS_DEFAULT_COLUMNS_SET = set(TIMDEXEmbeddings.DEFAULT_READ_COLUMNS) +EMBEDDINGS_AVAILABLE_COLUMNS_SET = set(TIMDEXEmbeddings.AVAILABLE_READ_COLUMNS) def test_dataset_embedding_init(): @@ -145,7 +145,7 @@ def test_embeddings_read_batches_yields_pyarrow_record_batches( def test_embeddings_read_batches_all_columns_by_default(timdex_embeddings_with_runs): batches = timdex_embeddings_with_runs.read_batches_iter() batch = next(batches) - assert set(batch.column_names) == EMBEDDINGS_DEFAULT_COLUMNS_SET + assert set(batch.column_names) == EMBEDDINGS_AVAILABLE_COLUMNS_SET def test_embeddings_read_batches_filter_columns(timdex_embeddings_with_runs): @@ -273,7 +273,7 @@ def test_embeddings_read_dicts_yields_dictionary_for_each_embeddings_record( dict_iter = timdex_embeddings_with_runs.read_dicts_iter() record = next(dict_iter) assert isinstance(record, dict) - assert set(record.keys()) == EMBEDDINGS_DEFAULT_COLUMNS_SET + assert set(record.keys()) == EMBEDDINGS_AVAILABLE_COLUMNS_SET def test_current_embeddings_view_single_run(timdex_dataset_for_embeddings_views): diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 6a2e4e0..da0ad69 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -4,12 +4,14 @@ import os from pathlib import Path +import pytest from duckdb import DuckDBPyConnection from tests.utils import generate_sample_embeddings_for_run, generate_sample_records from timdex_dataset_api import TIMDEXDataset +from timdex_dataset_api.data_source import TIMDEXDataSource from timdex_dataset_api.embeddings import TIMDEXEmbeddings -from timdex_dataset_api.metadata import DataTypeMetadataConfig, TIMDEXDatasetMetadata +from timdex_dataset_api.metadata import TIMDEXDatasetMetadata from timdex_dataset_api.records import TIMDEXRecords @@ -33,46 +35,53 @@ def test_tdm_s3_dataset_structure_properties(timdex_dataset_empty): assert timdex_dataset_empty.location_scheme == "file" -def test_data_type_metadata_config_prejoin_records_default_true(): - config = DataTypeMetadataConfig( - name="example", - metadata_columns=["timdex_record_id"], - data_path="data/example", - ) - assert config.prejoin_records is True - - -def test_data_source_metadata_configs_are_derived_from_base_class(): - assert TIMDEXRecords.METADATA_CONFIG.name == TIMDEXRecords.NAME - assert TIMDEXRecords.METADATA_CONFIG.data_path == TIMDEXRecords.DATA_PATH - assert TIMDEXRecords.METADATA_CONFIG.prejoin_records is False +def test_data_source_metadata_columns_are_derived_from_base_class(): assert ( - TIMDEXRecords.METADATA_CONFIG.metadata_columns + TIMDEXRecords.SOURCE_METADATA_COLUMNS == TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS ) + assert TIMDEXRecords.METADATA_COLUMNS == TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS - assert TIMDEXEmbeddings.METADATA_CONFIG.name == TIMDEXEmbeddings.NAME - assert TIMDEXEmbeddings.METADATA_CONFIG.data_path == TIMDEXEmbeddings.DATA_PATH - assert TIMDEXEmbeddings.METADATA_CONFIG.prejoin_records is True - assert TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns == [ + assert TIMDEXEmbeddings.SOURCE_METADATA_COLUMNS == [ "timdex_record_id", "run_id", "run_record_offset", - *TIMDEXEmbeddings.ADDITIONAL_METADATA_COLUMNS, "filename", + "embedding_timestamp", + "embedding_model", + "embedding_strategy", ] + assert [ + *TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS, + "embedding_timestamp", + "embedding_model", + "embedding_strategy", + ] == TIMDEXEmbeddings.METADATA_COLUMNS + + +def test_data_source_subclass_requires_contract_vars(): + with pytest.raises( + TypeError, + match=( + "InvalidDataSource must define required class vars: " + "SCHEMA, DATA_COLUMNS, DATA_PATH" + ), + ): + class InvalidDataSource(TIMDEXDataSource): + NAME = "invalid" -def test_dataset_registers_current_view_specs_from_data_sources(tmp_path): - td = TIMDEXDataset(str(tmp_path / "register_current_view_specs")) - expected_view_names = [ - spec.name - for spec in ( - TIMDEXRecords.CURRENT_VIEW_SPECS + TIMDEXEmbeddings.CURRENT_VIEW_SPECS - ) +def test_dataset_registers_table_configs_from_data_sources(tmp_path): + td = TIMDEXDataset(str(tmp_path / "register_table_configs")) + + expected_table_names = [ + table_config.name + for table_config in (TIMDEXRecords.TABLES + TIMDEXEmbeddings.TABLES) ] - assert [spec.name for spec in td.current_metadata_view_specs] == expected_view_names + assert [ + table_config.name for table_config in td.table_configs + ] == expected_table_names def test_tdm_create_metadata_database_file_success( @@ -136,9 +145,7 @@ def test_tdm_views_created_on_init(timdex_metadata): assert expected_views <= actual_views -def test_tdm_current_view_specs_missing_dependencies_are_skipped_generically( - caplog, tmp_path -): +def test_tdm_custom_tables_missing_dependencies_are_skipped_generically(caplog, tmp_path): dataset_path = str(tmp_path / "current_view_missing_dependencies") td = TIMDEXDataset(dataset_path) @@ -166,25 +173,28 @@ def test_tdm_current_view_specs_missing_dependencies_are_skipped_generically( """).to_df() metadata_names = set(metadata_objects.table_name) - missing_specs = [] - for spec in td_with_metadata.current_metadata_view_specs: + missing_tables = [] + for table_config in td_with_metadata.table_configs: + if table_config.kind != "custom": + continue + missing_required_tables = [ table_name - for table_name in spec.required_metadata_tables + for table_name in table_config.required_metadata_tables if table_name not in metadata_names ] if not missing_required_tables: continue - missing_specs.append(spec.name) - assert spec.name not in metadata_names + missing_tables.append(table_config.name) + assert table_config.name not in metadata_names assert ( "Skipping metadata." - f"{spec.name} view creation because missing dependencies: " + f"{table_config.name} view creation because missing dependencies: " f"{', '.join(missing_required_tables)}" ) in caplog.text - assert missing_specs + assert missing_tables def test_tdm_records_view_structure(timdex_metadata): @@ -374,7 +384,7 @@ def test_tdm_merge_append_deltas_static_counts_match_records_count_before_merge( def test_tdm_merge_append_deltas_adds_records_to_static_db( timdex_metadata_with_deltas, timdex_metadata_merged_deltas ): - columns = ",".join(TIMDEXRecords.METADATA_CONFIG.metadata_columns) + columns = ",".join(TIMDEXRecords.SOURCE_METADATA_COLUMNS) append_deltas = timdex_metadata_with_deltas.timdex_dataset.conn.query(f""" select {columns} @@ -396,10 +406,10 @@ def test_tdm_merge_append_deltas_deletes_append_deltas( timdex_metadata_with_deltas, timdex_metadata_merged_deltas ): records_deltas_path_before = timdex_metadata_with_deltas.append_deltas_path_for( - TIMDEXRecords.METADATA_CONFIG + TIMDEXRecords ) records_deltas_path_after = timdex_metadata_merged_deltas.append_deltas_path_for( - TIMDEXRecords.METADATA_CONFIG + TIMDEXRecords ) assert timdex_metadata_with_deltas.append_deltas_count != 0 @@ -436,14 +446,7 @@ def test_tdm_embeddings_metadata_view_structure(tmp_path): """select * from metadata.embeddings limit 1;""" ).to_df() assert len(embeddings_df) == 1 - # pre-joined view includes native embeddings columns + records columns - expected_columns = set(TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns) | { - "source", - "run_date", - "run_type", - "action", - "run_timestamp", - } + expected_columns = set(TIMDEXEmbeddings.METADATA_COLUMNS) assert set(embeddings_df.columns) == expected_columns @@ -475,14 +478,7 @@ def test_tdm_current_embeddings_view_structure(tmp_path): ).to_df() assert len(current_embeddings_df) == 1 - # pre-joined view includes native embeddings columns + records columns - expected_columns = set(TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns) | { - "source", - "run_date", - "run_type", - "action", - "run_timestamp", - } + expected_columns = set(TIMDEXEmbeddings.METADATA_COLUMNS) assert set(current_embeddings_df.columns) == expected_columns @@ -590,14 +586,7 @@ def test_tdm_current_run_embeddings_view_structure(tmp_path): ).to_df() assert len(current_run_embeddings_df) == 1 - # pre-joined view includes native embeddings columns + records columns - expected_columns = set(TIMDEXEmbeddings.METADATA_CONFIG.metadata_columns) | { - "source", - "run_date", - "run_type", - "action", - "run_timestamp", - } + expected_columns = set(TIMDEXEmbeddings.METADATA_COLUMNS) assert set(current_run_embeddings_df.columns) == expected_columns @@ -747,11 +736,7 @@ def test_tdm_keyset_paginated_query_on_prejoined_embeddings_view(tmp_path): # execute and verify results result_df = td.conn.query(query).to_df() assert len(result_df) == 10 # noqa: PLR2004 - expected_cols = set( - TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS - + TIMDEXEmbeddings.ADDITIONAL_METADATA_COLUMNS - + ["run_id_hash", "filename_hash"] - ) + expected_cols = {*TIMDEXEmbeddings.METADATA_COLUMNS, "run_id_hash", "filename_hash"} assert set(result_df.columns) == expected_cols @@ -783,9 +768,7 @@ def test_tdm_embeddings_write_append_deltas_without_static_embeddings_table(tmp_ """select count(*) from metadata.embeddings_append_deltas;""" ).fetchone()[0] - embeddings_deltas_path = td.metadata.append_deltas_path_for( - TIMDEXEmbeddings.METADATA_CONFIG - ) + embeddings_deltas_path = td.metadata.append_deltas_path_for(TIMDEXEmbeddings) assert embeddings_count == record_count assert embeddings_deltas_count == record_count assert os.listdir(embeddings_deltas_path) diff --git a/tests/test_read.py b/tests/test_read.py index c3ded3a..bf7b2d9 100644 --- a/tests/test_read.py +++ b/tests/test_read.py @@ -8,7 +8,7 @@ from timdex_dataset_api.records import TIMDEXRecords -DATASET_COLUMNS_SET = set(TIMDEXRecords.DEFAULT_READ_COLUMNS) +DATASET_COLUMNS_SET = set(TIMDEXRecords.AVAILABLE_READ_COLUMNS) def _count_rows_via_duckdb_parquet(timdex_dataset) -> int: diff --git a/tests/test_write.py b/tests/test_write.py index 6bc0612..0cd5839 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -159,7 +159,7 @@ def test_dataset_write_single_append_delta_success( ): written_files = timdex_dataset_empty.records.write(sample_records_generator(1_000)) records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for( - TIMDEXRecords.METADATA_CONFIG + TIMDEXRecords ) append_deltas = os.listdir(records_deltas_path) @@ -175,7 +175,7 @@ def test_dataset_write_multiple_append_deltas_success( written_files = timdex_dataset_empty.records.write(sample_records_generator(1_000)) records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for( - TIMDEXRecords.METADATA_CONFIG + TIMDEXRecords ) append_deltas = os.listdir(records_deltas_path) @@ -188,9 +188,9 @@ def test_dataset_write_append_delta_expected_metadata_columns( ): timdex_dataset_empty.records.write(sample_records_generator(1_000)) records_deltas_path = timdex_dataset_empty.metadata.append_deltas_path_for( - TIMDEXRecords.METADATA_CONFIG + TIMDEXRecords ) append_delta_filepath = os.listdir(records_deltas_path)[0] append_delta = pq.ParquetFile(Path(records_deltas_path) / append_delta_filepath) - assert append_delta.schema.names == TIMDEXRecords.METADATA_CONFIG.metadata_columns + assert append_delta.schema.names == TIMDEXRecords.SOURCE_METADATA_COLUMNS diff --git a/timdex_dataset_api/__init__.py b/timdex_dataset_api/__init__.py index 2633d63..67a794f 100644 --- a/timdex_dataset_api/__init__.py +++ b/timdex_dataset_api/__init__.py @@ -2,21 +2,16 @@ from importlib.metadata import version -from timdex_dataset_api.data_source import TIMDEXDataSource, ValidTable +from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource from timdex_dataset_api.dataset import TIMDEXDataset from timdex_dataset_api.embeddings import DatasetEmbedding, TIMDEXEmbeddings -from timdex_dataset_api.metadata import ( - CurrentMetadataViewSpec, - DataTypeMetadataConfig, - TIMDEXDatasetMetadata, -) +from timdex_dataset_api.metadata import TIMDEXDatasetMetadata from timdex_dataset_api.records import DatasetRecord, TIMDEXRecords __version__ = version("timdex_dataset_api") __all__ = [ - "CurrentMetadataViewSpec", - "DataTypeMetadataConfig", + "DataSourceTableConfig", "DatasetEmbedding", "DatasetRecord", "TIMDEXDataSource", @@ -24,5 +19,4 @@ "TIMDEXDatasetMetadata", "TIMDEXEmbeddings", "TIMDEXRecords", - "ValidTable", ] diff --git a/timdex_dataset_api/data_source.py b/timdex_dataset_api/data_source.py index 1258229..77e5b1d 100644 --- a/timdex_dataset_api/data_source.py +++ b/timdex_dataset_api/data_source.py @@ -12,19 +12,15 @@ import uuid from abc import ABC from collections.abc import Iterator -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, ClassVar, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, runtime_checkable import pandas as pd import pyarrow as pa import pyarrow.dataset as ds -from timdex_dataset_api.metadata import ( - CurrentMetadataViewSpec, - DataTypeMetadataConfig, - TIMDEXDatasetMetadata, -) +from timdex_dataset_api.metadata import TIMDEXDatasetMetadata if TYPE_CHECKING: from timdex_dataset_api.dataset import TIMDEXDataset @@ -33,14 +29,26 @@ @dataclass(frozen=True) -class ValidTable: - """A table or view that a data source exposes for reading.""" +class DataSourceTableConfig: + """Unified definition of a readable metadata-backed table or view.""" + # DuckDB table or view name, e.g. 'current_records'. name: str - """DuckDB table or view name, e.g. 'current_records'.""" + # Human-readable explanation of what this table contains. description: str - """Human-readable explanation of what this table contains.""" + + # Whether this is a base source table or a custom metadata view. + kind: Literal["base", "custom"] + + # DuckDB SQL to define custom projection + query_sql: str | None = None + + # List of metadata schema tables this relies on + required_metadata_tables: list[str] = field(default_factory=list) + + # Deprecated: to be removed + preload_setting_attribute: str | None = None @runtime_checkable @@ -74,8 +82,8 @@ class TIMDEXDataSource(ABC): # Heavy/data columns read from parquet data files DATA_COLUMNS: ClassVar[list[str]] - # Tables and views this data source exposes for reading - VALID_TABLES: ClassVar[list[ValidTable]] + # Tables this data source exposes for reading. + TABLES: ClassVar[list[DataSourceTableConfig]] = [] # ------------------------------------------------------------------ # # Optional sub-class class vars @@ -88,9 +96,6 @@ class TIMDEXDataSource(ABC): "day", ] - # Current-metadata view specs owned by this data source - CURRENT_VIEW_SPECS: ClassVar[list[CurrentMetadataViewSpec]] = [] - # Composite key columns used when joining metadata to parquet data. # filename is always included to physically disambiguate rows that share # the same logical key but reside in different parquet files (common for @@ -108,54 +113,56 @@ class TIMDEXDataSource(ABC): # ------------------------------------------------------------------ # # Derived class vars # ------------------------------------------------------------------ # - METADATA_CONFIG: ClassVar[DataTypeMetadataConfig] - ADDITIONAL_METADATA_COLUMNS: ClassVar[list[str]] - DEFAULT_READ_COLUMNS: ClassVar[list[str]] - VALID_READ_COLUMNS: ClassVar[set[str]] + METADATA_COLUMNS: ClassVar[list[str]] + SOURCE_METADATA_COLUMNS: ClassVar[list[str]] + AVAILABLE_READ_COLUMNS: ClassVar[list[str]] def __init_subclass__(cls, **kwargs: object) -> None: - """Instantiate DataSource subclasses.""" + """Build dynamic class variables for class.""" super().__init_subclass__(**kwargs) - # skip derivation for classes that haven't yet declared required contract vars + # validate that child class satisfies TIMDEXDataSource requirements required_class_vars = [ "NAME", "SCHEMA", "PARTITION_COLUMNS", "DATA_COLUMNS", "DATA_PATH", - "VALID_TABLES", + "TABLES", ] - if not all(hasattr(cls, var_name) for var_name in required_class_vars): - return + missing_class_vars = [ + var_name for var_name in required_class_vars if not hasattr(cls, var_name) + ] + if missing_class_vars: + missing = ", ".join(missing_class_vars) + raise TypeError(f"{cls.__name__} must define required class vars: {missing}") - cls.ADDITIONAL_METADATA_COLUMNS = cls.derive_additional_metadata_columns( - cls.SCHEMA.names, - cls.DATA_COLUMNS, - TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS, - cls.PARTITION_COLUMNS, - ) + schema_metadata_columns = [ + column_name + for column_name in cls.SCHEMA.names + if column_name not in cls.DATA_COLUMNS + and column_name not in cls.PARTITION_COLUMNS + ] - cls.DEFAULT_READ_COLUMNS = ( - TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS - + cls.ADDITIONAL_METADATA_COLUMNS - + cls.DATA_COLUMNS + cls.METADATA_COLUMNS = list( + dict.fromkeys( + TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS + schema_metadata_columns + ) ) - cls.VALID_READ_COLUMNS = set(cls.DEFAULT_READ_COLUMNS) - - cls.METADATA_CONFIG = DataTypeMetadataConfig( - name=cls.NAME, - metadata_columns=cls.derive_metadata_columns( - base_metadata_columns=TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS, - additional_metadata_columns=cls.ADDITIONAL_METADATA_COLUMNS, - prejoin_records_columns=TIMDEXDatasetMetadata.PREJOIN_RECORDS_COLUMNS, - prejoin_records=cls.PREJOIN_RECORDS, - ), - data_path=cls.DATA_PATH, - prejoin_records=cls.PREJOIN_RECORDS, + cls.AVAILABLE_READ_COLUMNS = list( + dict.fromkeys(cls.METADATA_COLUMNS + cls.DATA_COLUMNS) ) + if cls.PREJOIN_RECORDS: + cls.SOURCE_METADATA_COLUMNS = [ + column_name + for column_name in cls.METADATA_COLUMNS + if column_name not in TIMDEXDatasetMetadata.PREJOIN_RECORDS_COLUMNS + ] + else: + cls.SOURCE_METADATA_COLUMNS = cls.METADATA_COLUMNS + def __init__(self, timdex_dataset: "TIMDEXDataset") -> None: """Instance instantiation; runs after sub-class instantiation.""" self.timdex_dataset = timdex_dataset @@ -166,57 +173,13 @@ def __init__(self, timdex_dataset: "TIMDEXDataset") -> None: @property def data_root(self) -> str: """Root path for this source's parquet data.""" - return ( - f"{self.timdex_dataset.location.removesuffix('/')}" - f"/{self.METADATA_CONFIG.data_path}" - ) + return f"{self.timdex_dataset.location.removesuffix('/')}/{self.DATA_PATH}" @property def default_table(self) -> str: """Default table name for read methods.""" return self.NAME - @staticmethod - def derive_additional_metadata_columns( - schema_names: list[str], - data_columns: list[str], - base_metadata_columns: list[str], - partition_columns: list[str], - ) -> list[str]: - """Return additional metadata columns for a data source read contract. - - Derives columns from a physical parquet schema by excluding: - - payload/data columns - - shared/base metadata columns - - partition helper columns - """ - return [ - column_name - for column_name in schema_names - if column_name not in data_columns - and column_name not in base_metadata_columns - and column_name not in partition_columns - ] - - @staticmethod - def derive_metadata_columns( - base_metadata_columns: list[str], - additional_metadata_columns: list[str], - prejoin_records_columns: list[str], - *, - prejoin_records: bool, - ) -> list[str]: - """Return metadata columns stored in static/delta metadata tables.""" - if not prejoin_records: - return base_metadata_columns + additional_metadata_columns - - key_columns = [ - column_name - for column_name in base_metadata_columns - if column_name not in prejoin_records_columns and column_name != "filename" - ] - return key_columns + additional_metadata_columns + ["filename"] - def create_data_structure(self) -> None: """Ensure source data root exists (idempotent for local datasets).""" self._ensure_data_root_exists() @@ -274,7 +237,7 @@ def write( for written_file in written_files: self.timdex_dataset.metadata.write_append_delta( written_file.path, # type: ignore[attr-defined] - self.METADATA_CONFIG, + type(self), ) self.timdex_dataset.refresh() @@ -331,7 +294,7 @@ def read_batches_iter( Args: table: DuckDB table/view name (defaults to ``self.default_table``) - columns: columns to return (defaults to ``DEFAULT_READ_COLUMNS``) + columns: columns to return (defaults to ``AVAILABLE_READ_COLUMNS``) limit: max rows to yield where: raw SQL WHERE predicate **filters: key/value filter pairs @@ -339,10 +302,11 @@ def read_batches_iter( start_time = time.perf_counter() table = table or self.default_table - valid_table_names = {vt.name for vt in self.VALID_TABLES} + valid_table_names = {table_config.name for table_config in self.TABLES} if table not in valid_table_names: valid = ", ".join( - f"'{vt.name}' ({vt.description})" for vt in self.VALID_TABLES + f"'{table_config.name}' ({table_config.description})" + for table_config in self.TABLES ) raise ValueError(f"Invalid table: '{table}'. Valid tables: {valid}") @@ -455,12 +419,10 @@ def _build_data_query_for_chunk( registered_metadata_chunk: str = "meta_chunk", ) -> str: """Build SQL query for data retrieval, joining metadata chunk to parquet.""" - metadata_columns = ( - TIMDEXDatasetMetadata.BASE_METADATA_COLUMNS + self.ADDITIONAL_METADATA_COLUMNS - ) + metadata_columns = self.METADATA_COLUMNS - requested_columns = columns or self.DEFAULT_READ_COLUMNS - invalid_columns = set(requested_columns) - self.VALID_READ_COLUMNS + requested_columns = columns or self.AVAILABLE_READ_COLUMNS + invalid_columns = set(requested_columns) - set(self.AVAILABLE_READ_COLUMNS) if invalid_columns: invalid = ", ".join(sorted(invalid_columns)) raise ValueError(f"Invalid column: {invalid}") diff --git a/timdex_dataset_api/dataset.py b/timdex_dataset_api/dataset.py index 7f37676..564fdaa 100644 --- a/timdex_dataset_api/dataset.py +++ b/timdex_dataset_api/dataset.py @@ -92,18 +92,13 @@ def __init__( # create schemas self._create_duckdb_schemas() - source_classes = [TIMDEXRecords, TIMDEXEmbeddings] + self.source_classes = [TIMDEXRecords, TIMDEXEmbeddings] - # define which data types participate in metadata - self.data_type_configs = [ - source_class.METADATA_CONFIG for source_class in source_classes - ] - - # define current-row semantics for metadata current_* views - self.current_metadata_view_specs = [ - current_view_spec - for source_class in source_classes - for current_view_spec in source_class.CURRENT_VIEW_SPECS + # define readable metadata-backed tables contributed by data sources + self.table_configs = [ + table_config + for source_class in self.source_classes + for table_config in source_class.TABLES ] # composed components receive self diff --git a/timdex_dataset_api/embeddings.py b/timdex_dataset_api/embeddings.py index cc01325..509a444 100644 --- a/timdex_dataset_api/embeddings.py +++ b/timdex_dataset_api/embeddings.py @@ -5,8 +5,7 @@ import pyarrow as pa from attrs import asdict, define, field -from timdex_dataset_api.data_source import TIMDEXDataSource, ValidTable -from timdex_dataset_api.metadata import CurrentMetadataViewSpec +from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource from timdex_dataset_api.utils import datetime_iso_parse @@ -96,30 +95,6 @@ class TIMDEXEmbeddings(TIMDEXDataSource): DATA_PATH: ClassVar[str] = "data/embeddings" - VALID_TABLES: ClassVar[list[ValidTable]] = [ - ValidTable( - name="embeddings", - description="All embedding versions across all runs.", - ), - ValidTable( - name="current_embeddings", - description=( - "One row per (timdex_record_id, embedding_model," - " embedding_strategy) representing the most recent" - " embedding for each current record." - ), - ), - ValidTable( - name="current_run_embeddings", - description=( - "One row per (timdex_record_id, run_id, embedding_model," - " embedding_strategy) representing the most recent" - " embedding within each run, regardless of whether the" - " record is current." - ), - ), - ] - CURRENT_METADATA_VIEW_QUERY: ClassVar[str] = """ with -- CTE of embeddings attached to current record versions only @@ -159,14 +134,6 @@ class TIMDEXEmbeddings(TIMDEXDataSource): where rn = 1 """ - CURRENT_METADATA_VIEW_SPEC: ClassVar[CurrentMetadataViewSpec] = ( - CurrentMetadataViewSpec( - name="current_embeddings", - query_sql=CURRENT_METADATA_VIEW_QUERY, - required_metadata_tables=["embeddings", "current_records"], - ) - ) - CURRENT_RUN_METADATA_VIEW_QUERY: ClassVar[str] = """ with -- CTE of embeddings ranked by embedding recency within a run and family @@ -198,15 +165,32 @@ class TIMDEXEmbeddings(TIMDEXDataSource): where rn = 1 """ - CURRENT_RUN_METADATA_VIEW_SPEC: ClassVar[CurrentMetadataViewSpec] = ( - CurrentMetadataViewSpec( + TABLES: ClassVar[list[DataSourceTableConfig]] = [ + DataSourceTableConfig( + name="embeddings", + description="All embedding versions across all runs.", + kind="base", + ), + DataSourceTableConfig( + name="current_embeddings", + description=( + "One row per (timdex_record_id, embedding_model, " + "embedding_strategy) representing the most recent embedding " + "for each current record." + ), + kind="custom", + query_sql=CURRENT_METADATA_VIEW_QUERY, + required_metadata_tables=["embeddings", "current_records"], + ), + DataSourceTableConfig( name="current_run_embeddings", + description=( + "One row per (timdex_record_id, run_id, embedding_model, " + "embedding_strategy) representing the most recent embedding " + "within each run, regardless of whether the record is current." + ), + kind="custom", query_sql=CURRENT_RUN_METADATA_VIEW_QUERY, required_metadata_tables=["embeddings", "records"], - ) - ) - - CURRENT_VIEW_SPECS: ClassVar[list[CurrentMetadataViewSpec]] = [ - CURRENT_METADATA_VIEW_SPEC, - CURRENT_RUN_METADATA_VIEW_SPEC, + ), ] diff --git a/timdex_dataset_api/metadata.py b/timdex_dataset_api/metadata.py index 8db5c89..accff72 100644 --- a/timdex_dataset_api/metadata.py +++ b/timdex_dataset_api/metadata.py @@ -4,7 +4,6 @@ import shutil import tempfile import time -from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Unpack, cast @@ -23,52 +22,15 @@ ) if TYPE_CHECKING: + from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource from timdex_dataset_api.dataset import TIMDEXDataset from timdex_dataset_api.records import RecordsFilters logger = configure_logger(__name__) -@dataclass(frozen=True) -class DataTypeMetadataConfig: - """Configuration for a data type's participation in the metadata layer.""" - - name: str - """Identifier and static DB table name, e.g. 'records', 'embeddings'.""" - - metadata_columns: list[str] - """Ordered column names for the static DB table and append deltas. - These are the lightweight metadata columns — no large payloads - (no source_record, transformed_record, embedding_vector, etc.).""" - - data_path: str - """Location of data parquet files, e.g. "data/records.""" - - prejoin_records: bool = True - """If True, metadata union views pre-join to metadata.records, adding - source, run_date, run_type, action, run_timestamp as columns. - Set to False for 'records' (columns are native); True for bolt-on types.""" - - -@dataclass(frozen=True) -class CurrentMetadataViewSpec: - """Domain-owned definition for a current-metadata view.""" - - name: str - """View name created in metadata schema, e.g. 'current_records'.""" - - query_sql: str - """SQL query body used to create the view.""" - - required_metadata_tables: list[str] - """Metadata tables/views that must exist before creating this view.""" - - preload_setting_attribute: str | None = None - """Optional TIMDEXDataset bool attribute controlling temp-table preload.""" - - class TIMDEXDatasetMetadata: - """Class to handle metadata for all data types in the TIMDEXDataset.""" + """Class to handle metadata for all data sources in the TIMDEXDataset.""" BASE_METADATA_COLUMNS: ClassVar[list[str]] = [ "timdex_record_id", @@ -89,8 +51,8 @@ def __init__(self, timdex_dataset: "TIMDEXDataset") -> None: timdex_dataset: parent TIMDEXDataset instance """ self.timdex_dataset = timdex_dataset - self.data_type_configs = timdex_dataset.data_type_configs - self.current_metadata_view_specs = timdex_dataset.current_metadata_view_specs + self.source_classes = timdex_dataset.source_classes + self.table_configs = timdex_dataset.table_configs self.create_metadata_structure() self._setup_metadata_schema() @@ -107,47 +69,35 @@ def metadata_database_filename(self) -> str: def metadata_database_path(self) -> str: return f"{self.metadata_root}/{self.metadata_database_filename}" - def append_deltas_path_for(self, config: DataTypeMetadataConfig) -> str: - """Return the append deltas path for a specific data type.""" - return f"{self.metadata_root}/append_deltas/{config.name}" - - def get_config(self, name: str) -> DataTypeMetadataConfig: - """Lookup a DataTypeMetadataConfig by name.""" - for config in self.data_type_configs: - if config.name == name: - return config - raise ValueError(f"No metadata config for data type: {name}") - - def resolve_data_type_name_for_table(self, table: str) -> str: - """Resolve a metadata table/view name to its owning data type config name.""" - for config in self.data_type_configs: - if table == config.name or table.endswith(f"_{config.name}"): - return config.name - - raise ValueError(f"Could not resolve data type for metadata table '{table}'.") - - def data_type_metadata_columns_for(self, data_type_name: str) -> list[str]: - """Return type-specific metadata columns (excluding shared base metadata).""" - config = self.get_config(data_type_name) - return [ - column_name - for column_name in config.metadata_columns - if column_name not in self.BASE_METADATA_COLUMNS - ] + def append_deltas_path_for(self, source_class: type["TIMDEXDataSource"]) -> str: + """Return the append deltas path for a specific data source.""" + return f"{self.metadata_root}/append_deltas/{source_class.NAME}" + + def resolve_source_class_for_table(self, table: str) -> type["TIMDEXDataSource"]: + """Resolve a metadata table/view name to its owning data source class.""" + for source_class in self.source_classes: + if table == source_class.NAME or table.endswith(f"_{source_class.NAME}"): + return source_class + + raise ValueError(f"Could not resolve data source for metadata table '{table}'.") + + def data_source_metadata_columns_for( + self, source_class: type["TIMDEXDataSource"] + ) -> list[str]: + """Return the full metadata column surface for a data source.""" + return source_class.METADATA_COLUMNS def get_metadata_columns_for_table(self, table: str) -> list[str]: """Return canonical metadata columns projected by read keyset queries. - This method combines self.BASE_METADATA_COLUMNS with metadata columns specific to, - and identified by, the data class. + The returned columns are derived from the owning data source's metadata surface + and filtered to columns actually available on the requested table or view. """ sa_table = self.timdex_dataset.get_sa_table("metadata", table) available_columns = set(sa_table.c.keys()) - data_type_name = self.resolve_data_type_name_for_table(table) - type_metadata_columns = self.data_type_metadata_columns_for(data_type_name) - - expected_columns = self.BASE_METADATA_COLUMNS + type_metadata_columns + source_class = self.resolve_source_class_for_table(table) + expected_columns = self.data_source_metadata_columns_for(source_class) projected_columns: list[str] = [] for column_name in expected_columns: @@ -173,20 +123,20 @@ def current_records_count(self) -> int: select count(*) from metadata.current_records; """).fetchone()[0] # type: ignore[index] - def append_deltas_count_for(self, config: DataTypeMetadataConfig) -> int: - """Count append deltas rows for a single data type.""" - view_name = f"{config.name}_append_deltas" + def append_deltas_count_for(self, source_class: type["TIMDEXDataSource"]) -> int: + """Count append deltas rows for a single data source.""" + view_name = f"{source_class.NAME}_append_deltas" return self.timdex_dataset.conn.query(f""" select count(*) from metadata.{view_name}; """).fetchone()[0] # type: ignore[index] @property def append_deltas_count(self) -> int: - """Count of append deltas rows across all registered data types.""" + """Count of append deltas rows across all registered data sources.""" total = 0 - for config in self.data_type_configs: + for source_class in self.source_classes: try: - total += self.append_deltas_count_for(config) + total += self.append_deltas_count_for(source_class) except (DuckDBCatalogException, DuckDBBinderException): continue return total @@ -198,8 +148,8 @@ def create_metadata_structure(self) -> None: parents=True, exist_ok=True, ) - for config in self.data_type_configs: - Path(self.append_deltas_path_for(config)).mkdir( + for source_class in self.source_classes: + Path(self.append_deltas_path_for(source_class)).mkdir( parents=True, exist_ok=True, ) @@ -220,8 +170,8 @@ def rebuild_dataset_metadata(self) -> None: - build a local, temporary static metadata database file, then overwrite the canonical version in the dataset (e.g. in S3) """ - for config in self.data_type_configs: - deltas_path = self.append_deltas_path_for(config) + for source_class in self.source_classes: + deltas_path = self.append_deltas_path_for(source_class) if self.timdex_dataset.location_scheme == "s3": s3_client = S3Client() s3_client.delete_folder(deltas_path) @@ -252,30 +202,32 @@ def rebuild_dataset_metadata(self) -> None: self.timdex_dataset.refresh() def _create_full_dataset_table(self, conn: DuckDBPyConnection) -> None: - """Create metadata tables for all data types in the static database. + """Create metadata tables for all data sources in the static database. - Iterates over registered data type configs and creates one table per type. - Gracefully skips data types whose parquet data does not yet exist. + Iterates over registered data source classes and creates one table per source. + Gracefully skips data sources whose parquet data does not yet exist. """ - for config in self.data_type_configs: - self._create_metadata_table(conn, config) + for source_class in self.source_classes: + self._create_metadata_table(conn, source_class) def _create_metadata_table( - self, conn: DuckDBPyConnection, config: DataTypeMetadataConfig + self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"] ) -> None: - """Create a metadata table for a single data type in the static database.""" + """Create a metadata table for a single data source in the static database.""" start_time = time.perf_counter() - data_path = f"{self.timdex_dataset.location.removesuffix('/')}/{config.data_path}" + data_path = ( + f"{self.timdex_dataset.location.removesuffix('/')}/{source_class.DATA_PATH}" + ) - logger.debug(f"creating table static_db.main.{config.name}") + logger.debug(f"creating table static_db.main.{source_class.NAME}") # temporarily increase thread count for parallel parquet file scanning conn.execute("SET threads = 64;") try: sql_query = f""" - create or replace table {config.name} as ( - select {",".join(config.metadata_columns)} + create or replace table {source_class.NAME} as ( + select {",".join(source_class.SOURCE_METADATA_COLUMNS)} from read_parquet( '{data_path}/**/*.parquet', hive_partitioning=true, @@ -286,17 +238,17 @@ def _create_metadata_table( conn.execute(sql_query) except DuckDBIOException: logger.warning( - f"Could not create metadata table for '{config.name}' " + f"Could not create metadata table for '{source_class.NAME}' " f"(no parquet data at '{data_path}'). Skipping." ) return + finally: + # always reset thread count, even if parquet data is missing + conn.execute(f"""SET threads = {self.timdex_dataset.conn_factory.threads};""") - # reset thread count - conn.execute(f"""SET threads = {self.timdex_dataset.conn_factory.threads};""") - - row_count = conn.query(f"select count(*) from {config.name};").fetchone()[0] # type: ignore[index] + row_count = conn.query(f"select count(*) from {source_class.NAME};").fetchone()[0] # type: ignore[index] logger.info( - f"'{config.name}' table created - rows: {row_count}, " + f"'{source_class.NAME}' table created - rows: {row_count}, " f"elapsed: {time.perf_counter() - start_time}" ) @@ -317,12 +269,14 @@ def _setup_metadata_schema(self) -> None: self._attach_database_file(self.timdex_dataset.conn) - for config in self.data_type_configs: - self._create_append_deltas_view(self.timdex_dataset.conn, config) - self._create_union_view(self.timdex_dataset.conn, config) + for source_class in self.source_classes: + self._create_append_deltas_view(self.timdex_dataset.conn, source_class) + self._create_union_view(self.timdex_dataset.conn, source_class) - for spec in self.current_metadata_view_specs: - self._create_current_metadata_view(self.timdex_dataset.conn, spec) + for table_config in self.table_configs: + if table_config.kind != "custom": + continue + self._create_custom_metadata_table(self.timdex_dataset.conn, table_config) logger.debug( "Metadata schema setup for TIMDEXDatasetMetadata, " @@ -342,19 +296,19 @@ def _attach_database_file(self, conn: DuckDBPyConnection) -> None: ) def _create_append_deltas_view( - self, conn: DuckDBPyConnection, config: DataTypeMetadataConfig + self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"] ) -> None: - """Create a view that projects over append delta parquet files for a data type. + """Create a view that projects over append delta parquet files for a data source. If there are NO append deltas (e.g. after a rebuild or merge), we still create a - view by utilizing the schema from the static DB table but without any rows. This + view by utilizing the schema from the static DB table but without any rows. This allows downstream views to be built on top of this view. - The view is named ``metadata.{config.name}_append_deltas``. + The view is named ``metadata.{source_class.NAME}_append_deltas``. """ - view_name = f"{config.name}_append_deltas" - deltas_path = self.append_deltas_path_for(config) - static_table = f"static_db.{config.name}" + view_name = f"{source_class.NAME}_append_deltas" + deltas_path = self.append_deltas_path_for(source_class) + static_table = f"static_db.{source_class.NAME}" logger.debug(f"creating view metadata.{view_name}") @@ -363,7 +317,9 @@ def _create_append_deltas_view( select count(*) as file_count from glob('{deltas_path}/*.parquet') """).fetchone()[0] # type: ignore[index] - logger.debug(f"{append_delta_count} append deltas found for '{config.name}'") + logger.debug( + f"{append_delta_count} append deltas found for '{source_class.NAME}'" + ) # if deltas exist, always create this view from parquet files if append_delta_count > 0: @@ -382,7 +338,7 @@ def _create_append_deltas_view( table_exists = conn.execute(f""" select count(*) from information_schema.tables where table_catalog = 'static_db' - and table_name = '{config.name}' + and table_name = '{source_class.NAME}' """).fetchone()[0] # type: ignore[index] if table_exists: @@ -398,8 +354,8 @@ def _create_append_deltas_view( # no static table and no deltas, so no view to create logger.debug( - f"No static table or append deltas found for '{config.name}'; " - f"skipping append deltas view for '{config.name}'." + f"No static table or append deltas found for '{source_class.NAME}'; " + f"skipping append deltas view for '{source_class.NAME}'." ) # columns added to bolt-on types via pre-join to metadata.records @@ -412,34 +368,34 @@ def _create_append_deltas_view( ] def _create_union_view( - self, conn: DuckDBPyConnection, config: DataTypeMetadataConfig + self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"] ) -> None: - """Create a union view combining static DB and append deltas for a data type. + """Create a union view combining static DB and append deltas for a data source. - The view is named ``metadata.{config.name}`` and unions - ``static_db.{config.name}`` with ``metadata.{config.name}_append_deltas``. + The view is named ``metadata.{source_class.NAME}`` and unions + `static_db.{source_class.NAME}` with `metadata.{source_class.NAME}_append_deltas`. - For bolt-on data types (``config.prejoin_records=True``), the view pre-joins - to ``metadata.records`` so that ``source``, ``run_date``, ``run_type``, - ``action``, and ``run_timestamp`` are available as filterable columns. + For bolt-on data sources (`source_class.PREJOIN_RECORDS=True`), the view + pre-joins to `metadata.records` so that `source`, `run_date`, `run_type`, + `action`, and `run_timestamp` are available as filterable columns. """ - view_name = config.name - static_table = f"static_db.{config.name}" - deltas_view = f"metadata.{config.name}_append_deltas" - columns = ",".join(config.metadata_columns) + view_name = source_class.NAME + static_table = f"static_db.{source_class.NAME}" + deltas_view = f"metadata.{source_class.NAME}_append_deltas" + columns = ",".join(source_class.SOURCE_METADATA_COLUMNS) logger.debug(f"creating view metadata.{view_name}") static_table_exists = conn.execute(f""" select count(*) from information_schema.tables where table_catalog = 'static_db' - and table_name = '{config.name}' + and table_name = '{source_class.NAME}' """).fetchone()[0] # type: ignore[index] deltas_view_exists = conn.execute(f""" select count(*) from information_schema.tables where table_schema = 'metadata' - and table_name = '{config.name}_append_deltas' + and table_name = '{source_class.NAME}_append_deltas' and table_type = 'VIEW' """).fetchone()[0] # type: ignore[index] @@ -454,12 +410,12 @@ def _create_union_view( if base_subquery is None: logger.debug( - f"No static table or append deltas view found for '{config.name}'; " - f"skipping union view for '{config.name}'." + f"No static table or append deltas view found for '{source_class.NAME}'; " + f"skipping union view for '{source_class.NAME}'." ) return - if config.prejoin_records: + if source_class.PREJOIN_RECORDS: prejoin_cols = ",".join(f"r.{c}" for c in self.PREJOIN_RECORDS_COLUMNS) join_keys = "timdex_record_id, run_id, run_record_offset" conn.execute(f""" @@ -497,47 +453,54 @@ def _build_base_union_sql( return f"select {columns} from {deltas_view}" return None - def _create_current_metadata_view( - self, conn: DuckDBPyConnection, spec: CurrentMetadataViewSpec + def _create_custom_metadata_table( + self, conn: DuckDBPyConnection, table_config: "DataSourceTableConfig" ) -> None: - """Create a current metadata view from a registered domain-owned spec.""" + """Create a custom metadata view from a data-source table config.""" missing_tables = [ table_name - for table_name in spec.required_metadata_tables + for table_name in table_config.required_metadata_tables if not self._metadata_table_exists(conn, table_name) ] if missing_tables: logger.warning( - f"Skipping metadata.{spec.name} view creation because missing " + f"Skipping metadata.{table_config.name} view creation because missing " f"dependencies: {', '.join(missing_tables)}" ) return - logger.debug(f"creating view metadata.{spec.name}") + if table_config.query_sql is None: + raise ValueError( + f"Custom metadata table '{table_config.name}' must define query_sql." + ) + + logger.debug(f"creating view metadata.{table_config.name}") - if self._should_preload_current_view(spec): - logger.debug(f"creating temp table temp.main.{spec.name}") + if self._should_preload_table(table_config): + logger.debug(f"creating temp table temp.main.{table_config.name}") conn.execute("set temp_directory = '/tmp';") conn.execute(f""" - create or replace temp table temp.main.{spec.name} as - {spec.query_sql}; + create or replace temp table temp.main.{table_config.name} as + {table_config.query_sql}; - create or replace view metadata.{spec.name} as - select * from temp.main.{spec.name}; + create or replace view metadata.{table_config.name} as + select * from temp.main.{table_config.name}; """) return conn.execute(f""" - create or replace view metadata.{spec.name} as - {spec.query_sql}; + create or replace view metadata.{table_config.name} as + {table_config.query_sql}; """) - def _should_preload_current_view(self, spec: CurrentMetadataViewSpec) -> bool: - """Return True when a view spec is configured for temp-table preloading.""" - if spec.preload_setting_attribute is None: + def _should_preload_table(self, table_config: "DataSourceTableConfig") -> bool: + """Return True when a table config is configured for temp-table preloading.""" + if table_config.preload_setting_attribute is None: return False - return bool(getattr(self.timdex_dataset, spec.preload_setting_attribute, False)) + return bool( + getattr(self.timdex_dataset, table_config.preload_setting_attribute, False) + ) def _metadata_table_exists(self, conn: DuckDBPyConnection, table_name: str) -> bool: """Return True if a metadata schema table or view exists by name.""" @@ -551,7 +514,7 @@ def _metadata_table_exists(self, conn: DuckDBPyConnection, table_name: str) -> b def merge_append_deltas(self) -> None: """Merge append deltas into the static metadata database file. - Iterates over all data type configs, merging each type's deltas into its + Iterates over all data source configs, merging each source's deltas into its corresponding table in the static database. """ logger.info("merging append deltas into static metadata database file") @@ -560,11 +523,11 @@ def merge_append_deltas(self) -> None: s3_client = S3Client() - # collect all append delta filenames across all types + # collect all append delta filenames across all sources all_delta_filenames: dict[str, list[str]] = {} has_any_deltas = False - for config in self.data_type_configs: - deltas_view = f"{config.name}_append_deltas" + for source_class in self.source_classes: + deltas_view = f"{source_class.NAME}_append_deltas" try: filenames = ( self.timdex_dataset.conn.query(f""" @@ -581,7 +544,7 @@ def merge_append_deltas(self) -> None: KeyError, ): filenames = [] - all_delta_filenames[config.name] = filenames + all_delta_filenames[source_class.NAME] = filenames if filenames: has_any_deltas = True @@ -604,11 +567,11 @@ def merge_append_deltas(self) -> None: f"""attach '{local_db_path}' AS local_static_db;""" ) - # merge deltas for each data type - for config in self.data_type_configs: - if not all_delta_filenames[config.name]: + # merge deltas for each data source + for source_class in self.source_classes: + if not all_delta_filenames[source_class.NAME]: continue - self._merge_deltas_for_type(config) + self._merge_deltas_for_type(source_class) # detach from local static db self.timdex_dataset.conn.execute("""detach local_static_db;""") @@ -622,9 +585,9 @@ def merge_append_deltas(self) -> None: else: shutil.copy(src=local_db_path, dst=self.metadata_database_path) - # delete append deltas for all types - for config in self.data_type_configs: - for delta_filename in all_delta_filenames[config.name]: + # delete append deltas for all sources + for source_class in self.source_classes: + for delta_filename in all_delta_filenames[source_class.NAME]: if self.timdex_dataset.location_scheme == "s3": s3_client.delete_file(s3_uri=delta_filename) else: @@ -635,30 +598,30 @@ def merge_append_deltas(self) -> None: f"{self.metadata_database_path}, {time.perf_counter() - start_time}s" ) - def _merge_deltas_for_type(self, config: DataTypeMetadataConfig) -> None: - """Insert rows from append deltas into the local static DB for one data type.""" - columns = ",".join(config.metadata_columns) - deltas_view = f"metadata.{config.name}_append_deltas" + def _merge_deltas_for_type(self, source_class: type["TIMDEXDataSource"]) -> None: + """Insert rows from append deltas into the local static DB for one data source.""" + columns = ",".join(source_class.SOURCE_METADATA_COLUMNS) + deltas_view = f"metadata.{source_class.NAME}_append_deltas" - logger.debug(f"merging append deltas for '{config.name}'") + logger.debug(f"merging append deltas for '{source_class.NAME}'") # if type table doesn't yet exist in static DB, initialize it from deltas schema table_exists = self.timdex_dataset.conn.execute(f""" select count(*) from information_schema.tables where table_catalog = 'local_static_db' - and table_name = '{config.name}' + and table_name = '{source_class.NAME}' """).fetchone()[0] # type: ignore[index] if not table_exists: self.timdex_dataset.conn.execute(f""" - create table local_static_db.{config.name} as + create table local_static_db.{source_class.NAME} as select {columns} from {deltas_view} where 1 = 0 """) self.timdex_dataset.conn.execute(f""" - insert into local_static_db.{config.name} + insert into local_static_db.{source_class.NAME} select {columns} from {deltas_view} """) @@ -666,23 +629,23 @@ def _merge_deltas_for_type(self, config: DataTypeMetadataConfig) -> None: def write_append_delta( self, filepath: str, - config: DataTypeMetadataConfig, + source_class: type["TIMDEXDataSource"], ) -> None: """Write an append delta for a parquet file. A DuckDB context is used to read metadata-only columns from the parquet file, then write an append delta parquet file to - ``metadata/append_deltas/{config.name}/``. + ``metadata/append_deltas/{source_class.NAME}/``. Note: this operation is safe in parallel with other possible append delta writes. Args: filepath: path to the parquet file to extract metadata from - config: the DataTypeMetadataConfig for this data type + source_class: the data source class owning this parquet file """ start_time = time.perf_counter() - deltas_path = self.append_deltas_path_for(config) + deltas_path = self.append_deltas_path_for(source_class) output_path = f"{deltas_path}/append_delta-{filepath.split('/')[-1]}" # ensure s3:// schema prefix is present @@ -692,7 +655,7 @@ def write_append_delta( sql = f""" copy ( select - {",".join(config.metadata_columns)} + {",".join(source_class.SOURCE_METADATA_COLUMNS)} from read_parquet( '{filepath}', hive_partitioning=true, diff --git a/timdex_dataset_api/records.py b/timdex_dataset_api/records.py index 8d68810..4c94a5b 100644 --- a/timdex_dataset_api/records.py +++ b/timdex_dataset_api/records.py @@ -9,8 +9,7 @@ import pyarrow as pa from attrs import asdict, define, field -from timdex_dataset_api.data_source import TIMDEXDataSource, ValidTable -from timdex_dataset_api.metadata import CurrentMetadataViewSpec +from timdex_dataset_api.data_source import DataSourceTableConfig, TIMDEXDataSource from timdex_dataset_api.utils import ( datetime_iso_parse, strict_date_parse, @@ -110,21 +109,6 @@ class TIMDEXRecords(TIMDEXDataSource): PREJOIN_RECORDS: ClassVar[bool] = False - VALID_TABLES: ClassVar[list[ValidTable]] = [ - ValidTable( - name="records", - description="All record versions across all runs.", - ), - ValidTable( - name="current_records", - description=( - "One row per (source, timdex_record_id) representing the" - " most recent version of each record since the last full" - " run." - ), - ), - ] - CURRENT_METADATA_VIEW_QUERY: ClassVar[str] = """ with -- CTE of run_timestamp for last source full run @@ -167,17 +151,23 @@ class TIMDEXRecords(TIMDEXDataSource): where rn = 1 """ - CURRENT_METADATA_VIEW_SPEC: ClassVar[CurrentMetadataViewSpec] = ( - CurrentMetadataViewSpec( + TABLES: ClassVar[list[DataSourceTableConfig]] = [ + DataSourceTableConfig( + name="records", + description="All record versions across all runs.", + kind="base", + ), + DataSourceTableConfig( name="current_records", + description=( + "One row per (source, timdex_record_id) representing the " + "most recent version of each record since the last full run." + ), + kind="custom", query_sql=CURRENT_METADATA_VIEW_QUERY, required_metadata_tables=["records"], preload_setting_attribute="preload_current_records", - ) - ) - - CURRENT_VIEW_SPECS: ClassVar[list[CurrentMetadataViewSpec]] = [ - CURRENT_METADATA_VIEW_SPEC + ), ] def read_transformed_records_iter( From 4622073e1aa324101130dac76192247eb0f5a5e8 Mon Sep 17 00:00:00 2001 From: Graham Hukill Date: Tue, 14 Apr 2026 17:04:27 -0400 Subject: [PATCH 6/6] Bootstrap metadata from append deltas Why these changes are being introduced: For a brand new dataset there is a possible bootstrapping issue after first write. When attemping the first read, it formerly would fail because a metadata.db file did not yet exist. It would correctly log that it should be rebuilt, but that's not strictly necessary. With the append deltas written, we can project over the append deltas themselves for the metadata layer, just like we do inbetween metadata rebuilds or append delta merges. How this addresses that need: If no metadata.db is found, but there exist append deltas for a TIMDEXDataSource, proceed with metadata table creation, thereby skipping the need for an immediate metadata rebuild. Side effects of this change: * TDA is able to write and read from a brand new dataset location without a metadata rebuild. Relevant ticket(s): * https://mitlibraries.atlassian.net/browse/USE-496 --- tests/test_metadata.py | 39 +++ timdex_dataset_api/metadata.py | 69 ++++- uv.lock | 482 ++++++++++++++++----------------- 3 files changed, 336 insertions(+), 254 deletions(-) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index da0ad69..a9306d8 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -740,6 +740,45 @@ def test_tdm_keyset_paginated_query_on_prejoined_embeddings_view(tmp_path): assert set(result_df.columns) == expected_cols +def test_tdm_records_bootstrap_from_append_deltas_without_static_db(tmp_path): + record_count = 20 + td = TIMDEXDataset(str(tmp_path / "records_append_deltas_bootstrap")) + + td.records.write( + generate_sample_records( + num_records=record_count, + source="alma", + run_date="2025-03-01", + run_type="full", + run_id="records-bootstrap-run", + ) + ) + + assert td.metadata.database_exists() is False + assert len(td.records.read_dataframe()) == record_count + assert len(td.records.read_dataframe(table="current_records")) == record_count + + +def test_tdm_embeddings_bootstrap_from_append_deltas_without_static_db(tmp_path): + record_count = 20 + td = TIMDEXDataset(str(tmp_path / "embeddings_append_deltas_bootstrap")) + + td.records.write( + generate_sample_records( + num_records=record_count, + source="alma", + run_date="2025-03-02", + run_type="full", + run_id="emb-delta-run", + ) + ) + td.embeddings.write(generate_sample_embeddings_for_run(td, run_id="emb-delta-run")) + + assert td.metadata.database_exists() is False + assert len(td.embeddings.read_dataframe()) == record_count + assert len(td.embeddings.read_dataframe(table="current_embeddings")) == record_count + + def test_tdm_embeddings_write_append_deltas_without_static_embeddings_table(tmp_path): record_count = 20 td = TIMDEXDataset(str(tmp_path / "embeddings_append_deltas_only")) diff --git a/timdex_dataset_api/metadata.py b/timdex_dataset_api/metadata.py index accff72..cf38e9e 100644 --- a/timdex_dataset_api/metadata.py +++ b/timdex_dataset_api/metadata.py @@ -10,6 +10,7 @@ from duckdb import BinderException as DuckDBBinderException from duckdb import CatalogException as DuckDBCatalogException from duckdb import DuckDBPyConnection +from duckdb import HTTPException as DuckDBHTTPException from duckdb import IOException as DuckDBIOException from duckdb_engine import Dialect as DuckDBDialect from sqlalchemy import func, literal, select, text, tuple_ @@ -256,18 +257,35 @@ def _setup_metadata_schema(self) -> None: """Set up metadata schema views in the DuckDB connection. Creates views for accessing static metadata DB and append deltas. - If static DB doesn't exist, logs warning but doesn't fail. + If the static DB does not exist yet, bootstrap metadata views from append + deltas when available. """ start_time = time.perf_counter() - if not self.database_exists(): - logger.warning( - f"Static metadata database not found @ '{self.metadata_database_path}'. " - "Consider rebuild via TIMDEXDataset.metadata.rebuild_dataset_metadata()." - ) - return - - self._attach_database_file(self.timdex_dataset.conn) + if self.database_exists(): + self._attach_database_file(self.timdex_dataset.conn) + else: + bootstrap_sources = [ + source_class.NAME + for source_class in self.source_classes + if self._append_delta_count(self.timdex_dataset.conn, source_class) > 0 + ] + if bootstrap_sources: + logger.warning( + "Static metadata database not found @ " + f"'{self.metadata_database_path}'. " + "Bootstrapping metadata views from append deltas for: " + f"{', '.join(bootstrap_sources)}. " + "Consider rebuild via " + "TIMDEXDataset.metadata.rebuild_dataset_metadata()." + ) + else: + logger.warning( + "Static metadata database not found @ " + f"'{self.metadata_database_path}'. " + "Consider rebuild via " + "TIMDEXDataset.metadata.rebuild_dataset_metadata()." + ) for source_class in self.source_classes: self._create_append_deltas_view(self.timdex_dataset.conn, source_class) @@ -313,10 +331,7 @@ def _create_append_deltas_view( logger.debug(f"creating view metadata.{view_name}") # get current append delta count - append_delta_count = conn.execute(f""" - select count(*) as file_count - from glob('{deltas_path}/*.parquet') - """).fetchone()[0] # type: ignore[index] + append_delta_count = self._append_delta_count(conn, source_class) logger.debug( f"{append_delta_count} append deltas found for '{source_class.NAME}'" ) @@ -416,6 +431,13 @@ def _create_union_view( return if source_class.PREJOIN_RECORDS: + if not self._metadata_table_exists(conn, "records"): + logger.warning( + f"Skipping metadata.{view_name} view creation because missing " + "dependency: records" + ) + return + prejoin_cols = ",".join(f"r.{c}" for c in self.PREJOIN_RECORDS_COLUMNS) join_keys = "timdex_record_id, run_id, run_record_offset" conn.execute(f""" @@ -502,6 +524,27 @@ def _should_preload_table(self, table_config: "DataSourceTableConfig") -> bool: getattr(self.timdex_dataset, table_config.preload_setting_attribute, False) ) + def _append_delta_count( + self, conn: DuckDBPyConnection, source_class: type["TIMDEXDataSource"] + ) -> int: + """Return append delta parquet file count for a single data source.""" + deltas_glob = f"{self.append_deltas_path_for(source_class)}/*.parquet" + + try: + return cast( + "int", + conn.execute(f""" + select count(*) as file_count + from glob('{deltas_glob}') + """).fetchone()[0], # type: ignore[index] + ) + except (DuckDBHTTPException, DuckDBIOException): + logger.debug( + "Could not inspect append deltas for " + f"'{source_class.NAME}' at '{deltas_glob}'; assuming none exist." + ) + return 0 + def _metadata_table_exists(self, conn: DuckDBPyConnection, table_name: str) -> bool: """Return True if a metadata schema table or view exists by name.""" table_exists = conn.execute(f""" diff --git a/uv.lock b/uv.lock index 361158c..14e13d4 100644 --- a/uv.lock +++ b/uv.lock @@ -40,29 +40,29 @@ wheels = [ [[package]] name = "boto3" -version = "1.42.84" +version = "1.42.89" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore" }, { name = "jmespath" }, { name = "s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/88/89/2d647bd717da55a8cc68602b197f53a5fa36fb95a2f9e76c4aff11a9cfd1/boto3-1.42.84.tar.gz", hash = "sha256:6a84b3293a5d8b3adf827a54588e7dcffcf0a85410d7dadca615544f97d27579", size = 112816, upload-time = "2026-04-06T19:39:07.585Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/0c/f7bccb22b245cabf392816baba20f9e95f78ace7dbc580fd40136e80e732/boto3-1.42.89.tar.gz", hash = "sha256:3e43aacc0801bba9bcd23a8c271c089af297a69565f783fcdd357ae0e330bf1e", size = 113165, upload-time = "2026-04-13T19:36:17.516Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/31/cdf4326841613d1d181a77b3038a988800fb3373ca50de1639fba9fa87de/boto3-1.42.84-py3-none-any.whl", hash = "sha256:4d03ad3211832484037337292586f71f48707141288d9ac23049c04204f4ab03", size = 140555, upload-time = "2026-04-06T19:39:06.009Z" }, + { url = "https://files.pythonhosted.org/packages/b9/33/55103ba5ef9975ea54b8d39e69b76eb6e9fded3beae5f01065e26951a3a1/boto3-1.42.89-py3-none-any.whl", hash = "sha256:6204b189f4d0c655535f43d7eaa57ff4e8d965b8463c97e45952291211162932", size = 140556, upload-time = "2026-04-13T19:36:13.894Z" }, ] [[package]] name = "boto3-stubs" -version = "1.42.84" +version = "1.42.89" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "botocore-stubs" }, { name = "types-s3transfer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/97/7a/5c7cd6b60345592319ea2db2ee4147ff7b2b25456ffd2cf05e29c293cb9e/boto3_stubs-1.42.84.tar.gz", hash = "sha256:c517c254e1d8f00af24f7df55c8b1061d1142405c5ac07e426ee2b5b709f3362", size = 102186, upload-time = "2026-04-06T20:07:17.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/7d/2ea3bb15bba37e6d70f9297dccd1ce769b7b92f6179baa890293995f359b/boto3_stubs-1.42.89.tar.gz", hash = "sha256:dbbc4fd2678cfb21da9bab1b5e30ba951852322d055045ac12042ba34d04597a", size = 102703, upload-time = "2026-04-13T19:51:49.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/af/90281a0333536548d9cb2d452116ae27e2e16a26096fcc999666e172f6e5/boto3_stubs-1.42.84-py3-none-any.whl", hash = "sha256:73c3f47fc18e27dfe6f17c1c4d3ee48ab6f926d1b7029d15e6771c8255a6f278", size = 70448, upload-time = "2026-04-06T20:07:15.265Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5d/397d2393cba13b6764da0da0aee5a16d05b304de081dfec1be69829ef0d4/boto3_stubs-1.42.89-py3-none-any.whl", hash = "sha256:699e510078a057766e2de1d2d91d99dac2ce3ca2d4e6adf8df27b305d04b91d2", size = 70667, upload-time = "2026-04-13T19:51:43.131Z" }, ] [package.optional-dependencies] @@ -72,16 +72,16 @@ s3 = [ [[package]] name = "botocore" -version = "1.42.84" +version = "1.42.89" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jmespath" }, { name = "python-dateutil" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/b7/1c03423843fb0d1795b686511c00ee63fed1234c2400f469aeedfd42212f/botocore-1.42.84.tar.gz", hash = "sha256:234064604c80d9272a5e9f6b3566d260bcaa053a5e05246db90d7eca1c2cf44b", size = 15148615, upload-time = "2026-04-06T19:38:56.673Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/cc/e6be943efa9051bd15c2ee14077c2b10d6e27c9e9385fc43a03a5c4ed8b5/botocore-1.42.89.tar.gz", hash = "sha256:95ac52f472dad29942f3088b278ab493044516c16dbf9133c975af16527baa99", size = 15206290, upload-time = "2026-04-13T19:36:02.321Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/37/0c0c90361c8a1b9e6c75222ca24ae12996a298c0e18822a72ab229c37207/botocore-1.42.84-py3-none-any.whl", hash = "sha256:15f3fe07dfa6545e46a60c4b049fe2bdf63803c595ae4a4eec90e8f8172764f3", size = 14827061, upload-time = "2026-04-06T19:38:53.613Z" }, + { url = "https://files.pythonhosted.org/packages/91/f1/90a7b8eda38b7c3a65ca7ee0075bdf310b6b471cb1b95fab6e8994323a50/botocore-1.42.89-py3-none-any.whl", hash = "sha256:d9b786c8d9db6473063b4cc5be0ba7e6a381082307bd6afb69d4216f9fa95f35", size = 14887287, upload-time = "2026-04-13T19:35:56.677Z" }, ] [[package]] @@ -383,55 +383,55 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.6" +version = "46.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, - { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, - { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, - { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, - { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, - { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, - { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, - { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, - { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, - { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, - { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, - { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, - { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, - { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, - { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, - { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, - { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, - { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, - { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, - { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, - { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, - { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, - { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, - { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, ] [[package]] @@ -479,31 +479,31 @@ wheels = [ [[package]] name = "duckdb" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/62/590caabec6c41003f46a244b6fd707d35ca2e552e0c70cbf454e08bf6685/duckdb-1.5.1.tar.gz", hash = "sha256:b370d1620a34a4538ef66524fcee9de8171fa263c701036a92bc0b4c1f2f9c6d", size = 17995082, upload-time = "2026-03-23T12:12:15.894Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/06/be4c62f812c6e23898733073ace0482eeb18dffabe0585d63a3bf38bca1e/duckdb-1.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6f7361d66cc801d9eb4df734b139cd7b0e3c257a16f3573ebd550ddb255549e6", size = 30113703, upload-time = "2026-03-23T12:11:02.536Z" }, - { url = "https://files.pythonhosted.org/packages/44/03/1794dcdda75ff203ab0982ff7eb5232549b58b9af66f243f1b7212d6d6be/duckdb-1.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6acc2040bec1f05de62a2f3f68f4c12f3ec7d6012b4317d0ab1a195af26225", size = 15991802, upload-time = "2026-03-23T12:11:06.321Z" }, - { url = "https://files.pythonhosted.org/packages/87/03/293bccd838a293d42ea26dec7f4eb4f58b57b6c9ffcfabc6518a5f20a24a/duckdb-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed6d23a3f806898e69c77430ebd8da0c79c219f97b9acbc9a29a653e09740c59", size = 14246803, upload-time = "2026-03-23T12:11:09.624Z" }, - { url = "https://files.pythonhosted.org/packages/15/2c/7b4f11879aa2924838168b4640da999dccda1b4a033d43cb998fd6dc33ea/duckdb-1.5.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6af347debc8b721aa72e48671166282da979d5e5ae52dbc660ab417282b48e23", size = 19271654, upload-time = "2026-03-23T12:11:13.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d6/8f9a6b1fbcc669108ec6a4d625a70be9e480b437ed9b70cd56b78cd577a6/duckdb-1.5.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8150c569b2aa4573b51ba8475e814aa41fd53a3d510c1ffb96f1139f46faf611", size = 21386100, upload-time = "2026-03-23T12:11:16.758Z" }, - { url = "https://files.pythonhosted.org/packages/c4/fe/8d02c6473273468cf8d43fd5d73c677f8cdfcd036c1e884df0613f124c2b/duckdb-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:054ad424b051b334052afac58cb216f3b1ebb8579fc8c641e60f0182e8725ea9", size = 13083506, upload-time = "2026-03-23T12:11:19.785Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/2be786b9c153eb263bf5d3d5f7ab621b14a715d7e70f92b24ecf8536369e/duckdb-1.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:6ba302115f63f6482c000ccfd62efdb6c41d9d182a5bcd4a90e7ab8cd13856eb", size = 13888862, upload-time = "2026-03-23T12:11:22.84Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f2/af476945e3b97417945b0f660b5efa661863547c0ea104251bb6387342b1/duckdb-1.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:26e56b5f0c96189e3288d83cf7b476e23615987902f801e5788dee15ee9f24a9", size = 30113759, upload-time = "2026-03-23T12:11:26.5Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9d/5a542b3933647369e601175190093597ce0ac54909aea0dd876ec51ffad4/duckdb-1.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:972d0dbf283508f9bc446ee09c3838cb7c7f114b5bdceee41753288c97fe2f7c", size = 15991463, upload-time = "2026-03-23T12:11:30.025Z" }, - { url = "https://files.pythonhosted.org/packages/53/a5/b59cff67f5e0420b8f337ad86406801cffacae219deed83961dcceefda67/duckdb-1.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:482f8a13f2600f527e427f73c42b5aa75536f9892868068f0aaf573055a0135f", size = 14246482, upload-time = "2026-03-23T12:11:33.33Z" }, - { url = "https://files.pythonhosted.org/packages/e9/12/d72a82fe502aae82b97b481bf909be8e22db5a403290799ad054b4f90eb4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da137802688190835b4c863cafa77fd7e29dff662ee6d905a9ffc14f00299c91", size = 19270816, upload-time = "2026-03-23T12:11:36.79Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c3/ee49319b15f139e04c067378f0e763f78336fbab38ba54b0852467dd9da4/duckdb-1.5.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d4147422d91ccdc2d2abf6ed24196025e020259d1d267970ae20c13c2ce84b1", size = 21385695, upload-time = "2026-03-23T12:11:40.465Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f5/a15498e75a27a136c791ca1889beade96d388dadf9811375db155fc96d1a/duckdb-1.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:05fc91767d0cfc4cf2fa68966ab5b479ac07561752e42dd0ae30327bd160f64a", size = 13084065, upload-time = "2026-03-23T12:11:43.763Z" }, - { url = "https://files.pythonhosted.org/packages/93/81/b3612d2bbe237f75791095e16767c61067ea5d31c76e8591c212dac13bd0/duckdb-1.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:a28531cee2a5a42d89f9ba4da53bfeb15681f12acc0263476c8705380dadce07", size = 13892892, upload-time = "2026-03-23T12:11:47.222Z" }, - { url = "https://files.pythonhosted.org/packages/ad/75/e9e7893542ca738bcde2d41d459e3438950219c71c57ad28b049dc2ae616/duckdb-1.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:eba81e0b3011c1f23df7ea47ef4ffaa8239817959ae291515b6efd068bde2161", size = 30123677, upload-time = "2026-03-23T12:11:51.511Z" }, - { url = "https://files.pythonhosted.org/packages/df/db/f7420ee7109a922124c02f377ae1c56156e9e4aa434f4726848adaef0219/duckdb-1.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:afab8b4b1f4469c3879bb049dd039f8fce402712050324e9524a43d7324c5e87", size = 15996808, upload-time = "2026-03-23T12:11:54.964Z" }, - { url = "https://files.pythonhosted.org/packages/df/57/2c4c3de1f1110417592741863ba58b4eca2f7690a421712762ddbdcd72e6/duckdb-1.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:71dddcebbc5a70e946a06c30b59b5dd7999c9833d307168f90fb4e4b672ab63e", size = 14248990, upload-time = "2026-03-23T12:11:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e173b33ffac53124a3e39e97fb60a538f26651a0df6e393eb9bf7540126c/duckdb-1.5.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac2804043bd1bc10b5da18f8f4c706877197263a510c41be9b4c0062f5783dcc", size = 19276013, upload-time = "2026-03-23T12:12:02.034Z" }, - { url = "https://files.pythonhosted.org/packages/d4/4c/47e838393aa90d3d78549c8c04cb09452efeb14aaae0ee24dc0bd61c3a41/duckdb-1.5.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8843bd9594e1387f1e601439e19ad73abdf57356104fd1e53a708255bb95a13d", size = 21387569, upload-time = "2026-03-23T12:12:05.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9b/ce65743e0e85f5c984d2f7e8a81bc908d0bac345d6d8b6316436b29430e7/duckdb-1.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:d68c5a01a283cb13b79eafe016fe5869aa11bff8c46e7141c70aa0aac808010f", size = 13603876, upload-time = "2026-03-23T12:12:09.344Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ac/f9e4e731635192571f86f52d86234f537c7f8ca4f6917c56b29051c077ef/duckdb-1.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:a3be2072315982e232bfe49c9d3db0a59ba67b2240a537ef42656cc772a887c7", size = 14370790, upload-time = "2026-03-23T12:12:12.497Z" }, +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/66/744b4931b799a42f8cb9bc7a6f169e7b8e51195b62b246db407fd90bf15f/duckdb-1.5.2.tar.gz", hash = "sha256:638da0d5102b6cb6f7d47f83d0600708ac1d3cb46c5e9aaabc845f9ba4d69246", size = 18017166, upload-time = "2026-04-13T11:30:09.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/de/ebe66bbe78125fc610f4fd415447a65349d94245950f3b3dfb31d028af02/duckdb-1.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e6495b00cad16888384119842797c49316a96ae1cb132bb03856d980d95afee1", size = 30064950, upload-time = "2026-04-13T11:29:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8a/3e25b5d03bcf1fb99d189912f8ce92b1db4f9c8778e1b1f55745973a855a/duckdb-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d72b8856b1839d35648f38301b058f6232f4d36b463fe4dc8f4d3fdff2df1a2e", size = 15969113, upload-time = "2026-04-13T11:29:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/58001f0815002b1a93431bf907f77854085c7d049b83d521814a07b9db0b/duckdb-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a1de4f4d454b8c97aec546c82003fc834d3422ce4bc6a19902f3462ef293bed", size = 14224774, upload-time = "2026-04-13T11:29:16.758Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2f/a7f0de9509d1cef35608aeb382919041cdd70f58c173865c3da6a0d87979/duckdb-1.5.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce0b8141a10d37ecef729c45bc41d334854013f4389f1488bd6035c5579aaac1", size = 19313510, upload-time = "2026-04-13T11:29:19.574Z" }, + { url = "https://files.pythonhosted.org/packages/26/78/eb1e064ea8b9df3b87b167bfd7a407b2f615a4291e06cba756727adfa06c/duckdb-1.5.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99ef73a277c8921bc0a1f16dee38d924484251d9cfd20951748c20fcd5ed855", size = 21429692, upload-time = "2026-04-13T11:29:22.575Z" }, + { url = "https://files.pythonhosted.org/packages/5b/12/05b0c47d14839925c5e35b79081d918ca82e3f236bb724a6f58409dd5291/duckdb-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:8d599758b4e48bf12e18c9b960cf491d219f0c4972d19a45489c05cc5ab36f83", size = 13107594, upload-time = "2026-04-13T11:29:25.43Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2c/80558a82b236e044330e84a154b96aacddb343316b479f3d49be03ea11cb/duckdb-1.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:fc85a5dbcbe6eccac1113c72370d1d3aacfdd49198d63950bdf7d8638a307f00", size = 13927537, upload-time = "2026-04-13T11:29:27.842Z" }, + { url = "https://files.pythonhosted.org/packages/98/f2/e3d742808f138d374be4bb516fade3d1f33749b813650810ab7885cdc363/duckdb-1.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4420b3f47027a7849d0e1815532007f377fa95ee5810b47ea717d35525c12f79", size = 30064879, upload-time = "2026-04-13T11:29:30.763Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/f3dc1cf97e1267ca15e4307d456f96ce583961f0703fd75e62b2ad8d64fa/duckdb-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb42e6ed543902e14eae647850da24103a89f0bc2587dec5601b1c1f213bd2ed", size = 15969327, upload-time = "2026-04-13T11:29:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e0/d5418def53ae4e05a63075705ff44ed5af5a1a5932627eb2b600c5df1c93/duckdb-1.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98c0535cd6d901f61a5ea3c2e26a1fd28482953d794deb183daf568e3aa5dda6", size = 14225107, upload-time = "2026-04-13T11:29:35.882Z" }, + { url = "https://files.pythonhosted.org/packages/16/a7/15aaa59dbecc35e9711980fcdbf525b32a52470b32d18ef678193a146213/duckdb-1.5.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:486c862bf7f163c0110b6d85b3e5c031d224a671cca468f12ebb1d3a348f6b39", size = 19313433, upload-time = "2026-04-13T11:29:38.367Z" }, + { url = "https://files.pythonhosted.org/packages/bd/21/d903cc63a5140c822b7b62b373a87dc557e60c29b321dfb435061c5e67cf/duckdb-1.5.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70631c847ca918ee710ec874241b00cf9d2e5be90762cbb2a0389f17823c08f7", size = 21429837, upload-time = "2026-04-13T11:29:41.135Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0a/b770d1f60c70597302130d6247f418549b7094251a02348fbaf1c7e147ae/duckdb-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:52a21823f3fbb52f0f0e5425e20b07391ad882464b955879499b5ff0b45a376b", size = 13107699, upload-time = "2026-04-13T11:29:43.905Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/e200fe431d700962d1a908d2ce89f53ccee1cc8db260174ae663ba09686b/duckdb-1.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:411ad438bd4140f189a10e7f515781335962c5d18bd07837dc6d202e3985253d", size = 13927646, upload-time = "2026-04-13T11:29:46.598Z" }, + { url = "https://files.pythonhosted.org/packages/83/a1/f6286c67726cc1ea60a6e3c0d9fbc66527dde24ae089a51bbe298b13ca78/duckdb-1.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6b0fe75c148000f060aa1a27b293cacc0ea08cc1cad724fbf2143d56070a3785", size = 30078598, upload-time = "2026-04-13T11:29:49.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/59febb02f21a4a5c6b0b0099ef7c965fdd5e61e4904cf813809bb792e35f/duckdb-1.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35579b8e3a064b5eaf15b0eafc558056a13f79a0a62e34cc4baf57119daecfec", size = 15975120, upload-time = "2026-04-13T11:29:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/09/70/ce750854d37bb5a45cccbb2c3cb04df4af56aea8fc30a2499bb643b4a9c0/duckdb-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea58ff5b0880593a280cf5511734b17711b32ee1f58b47d726e8600848358160", size = 14227762, upload-time = "2026-04-13T11:29:55.564Z" }, + { url = "https://files.pythonhosted.org/packages/28/dc/ad45ac3c0b6c4687dc649e8f6cf01af1c8b0443932a39b2abb4ebcb3babd/duckdb-1.5.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef461bca07313412dc09961c4a4757a851f56b95ac01c58fac6007632b7b94f2", size = 19315668, upload-time = "2026-04-13T11:29:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b1/1464f468d2e5813f5808de95df9d3113a645a5bfa2ffcaecbc542ddae272/duckdb-1.5.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be37680ddb380015cb37318e378c53511c45c4f0d8fac5599d22b7d092b9217a", size = 21434056, upload-time = "2026-04-13T11:30:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/ce/32/6673607e024722473fa7aafdd29c0e3dd231dd528f6cd8b5797fbeeb229d/duckdb-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:0b291786014df1133f8f18b9df4d004484613146e858d71a21791e0fcca16cf4", size = 13633667, upload-time = "2026-04-13T11:30:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e3/9d34173ec068631faea3ea6e73050700729363e7e33306a9a3218e5cdc61/duckdb-1.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:c9f3e0b71b8a50fccfb42794899285d9d318ce2503782b9dd54868e5ecd0ad31", size = 14402513, upload-time = "2026-04-13T11:30:06.609Z" }, ] [[package]] @@ -540,41 +540,41 @@ wheels = [ [[package]] name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/94/a5935717b307d7c71fe877b52b884c6af707d2d2090db118a03fbd799369/greenlet-3.4.0.tar.gz", hash = "sha256:f50a96b64dafd6169e595a5c56c9146ef80333e67d4476a65a9c55f400fc22ff", size = 195913, upload-time = "2026-04-08T17:08:00.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/8b/3669ad3b3f247a791b2b4aceb3aa5a31f5f6817bf547e4e1ff712338145a/greenlet-3.4.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1a54a921561dd9518d31d2d3db4d7f80e589083063ab4d3e2e950756ef809e1a", size = 286902, upload-time = "2026-04-08T15:52:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/38/3e/3c0e19b82900873e2d8469b590a6c4b3dfd2b316d0591f1c26b38a4879a5/greenlet-3.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16dec271460a9a2b154e3b1c2fa1050ce6280878430320e85e08c166772e3f97", size = 606099, upload-time = "2026-04-08T16:24:38.408Z" }, + { url = "https://files.pythonhosted.org/packages/b5/33/99fef65e7754fc76a4ed14794074c38c9ed3394a5bd129d7f61b705f3168/greenlet-3.4.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90036ce224ed6fe75508c1907a77e4540176dcf0744473627785dd519c6f9996", size = 618837, upload-time = "2026-04-08T16:30:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/229f3aed6948faa20e0616a0b8568da22e365ede6a54d7d369058b128afd/greenlet-3.4.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1c4f6b453006efb8310affb2d132832e9bbb4fc01ce6df6b70d810d38f1f6dc", size = 615062, upload-time = "2026-04-08T15:56:33.766Z" }, + { url = "https://files.pythonhosted.org/packages/08/97/d988180011aa40135c46cd0d0cf01dd97f7162bae14139b4a3ef54889ba5/greenlet-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b2d9a138ffa0e306d0e2b72976d2fb10b97e690d40ab36a472acaab0838e2de", size = 1573511, upload-time = "2026-04-08T16:26:20.058Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0f/a5a26fe152fb3d12e6a474181f6e9848283504d0afd095f353d85726374b/greenlet-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8424683caf46eb0eb6f626cb95e008e8cc30d0cb675bdfa48200925c79b38a08", size = 1640396, upload-time = "2026-04-08T15:57:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/42/cf/bb2c32d9a100e36ee9f6e38fad6b1e082b8184010cb06259b49e1266ca01/greenlet-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0a53fb071531d003b075c444014ff8f8b1a9898d36bb88abd9ac7b3524648a2", size = 238892, upload-time = "2026-04-08T17:03:10.094Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/6c41314bac56e71436ce551c7fbe3cc830ed857e6aa9708dbb9c65142eb6/greenlet-3.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:f38b81880ba28f232f1f675893a39cf7b6db25b31cc0a09bb50787ecf957e85e", size = 235599, upload-time = "2026-04-08T15:52:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/7a/75/7e9cd1126a1e1f0cd67b0eda02e5221b28488d352684704a78ed505bd719/greenlet-3.4.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:43748988b097f9c6f09364f260741aa73c80747f63389824435c7a50bfdfd5c1", size = 285856, upload-time = "2026-04-08T15:52:45.82Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c4/3e2df392e5cb199527c4d9dbcaa75c14edcc394b45040f0189f649631e3c/greenlet-3.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5566e4e2cd7a880e8c27618e3eab20f3494452d12fd5129edef7b2f7aa9a36d1", size = 610208, upload-time = "2026-04-08T16:24:39.674Z" }, + { url = "https://files.pythonhosted.org/packages/da/af/750cdfda1d1bd30a6c28080245be8d0346e669a98fdbae7f4102aa95fff3/greenlet-3.4.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1054c5a3c78e2ab599d452f23f7adafef55062a783a8e241d24f3b633ba6ff82", size = 621269, upload-time = "2026-04-08T16:30:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/0cbc693622cd54ebe25207efbb3a0eb07c2639cb8594f6e3aaaa0bb077a8/greenlet-3.4.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f82cb6cddc27dd81c96b1506f4aa7def15070c3b2a67d4e46fd19016aacce6cf", size = 617549, upload-time = "2026-04-08T15:56:34.893Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/8966767de01343c1ff47e8b855dc78e7d1a8ed2b7b9c83576a57e289f81d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:227a46251ecba4ff46ae742bc5ce95c91d5aceb4b02f885487aff269c127a729", size = 1575310, upload-time = "2026-04-08T16:26:21.671Z" }, + { url = "https://files.pythonhosted.org/packages/b8/38/bcdc71ba05e9a5fda87f63ffc2abcd1f15693b659346df994a48c968003d/greenlet-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5b99e87be7eba788dd5b75ba1cde5639edffdec5f91fe0d734a249535ec3408c", size = 1640435, upload-time = "2026-04-08T15:57:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c2/19b664b7173b9e4ef5f77e8cef9f14c20ec7fce7920dc1ccd7afd955d093/greenlet-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:849f8bc17acd6295fcb5de8e46d55cc0e52381c56eaf50a2afd258e97bc65940", size = 238760, upload-time = "2026-04-08T17:04:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/9b/96/795619651d39c7fbd809a522f881aa6f0ead504cc8201c3a5b789dfaef99/greenlet-3.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9390ad88b652b1903814eaabd629ca184db15e0eeb6fe8a390bbf8b9106ae15a", size = 235498, upload-time = "2026-04-08T17:05:00.584Z" }, + { url = "https://files.pythonhosted.org/packages/78/02/bde66806e8f169cf90b14d02c500c44cdbe02c8e224c9c67bafd1b8cadd1/greenlet-3.4.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:10a07aca6babdd18c16a3f4f8880acfffc2b88dfe431ad6aa5f5740759d7d75e", size = 286291, upload-time = "2026-04-08T17:09:34.307Z" }, + { url = "https://files.pythonhosted.org/packages/05/1f/39da1c336a87d47c58352fb8a78541ce63d63ae57c5b9dae1fe02801bbc2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:076e21040b3a917d3ce4ad68fb5c3c6b32f1405616c4a57aa83120979649bd3d", size = 656749, upload-time = "2026-04-08T16:24:41.721Z" }, + { url = "https://files.pythonhosted.org/packages/d3/6c/90ee29a4ee27af7aa2e2ec408799eeb69ee3fcc5abcecac6ddd07a5cd0f2/greenlet-3.4.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e82689eea4a237e530bb5cb41b180ef81fa2160e1f89422a67be7d90da67f615", size = 669084, upload-time = "2026-04-08T16:31:01.372Z" }, + { url = "https://files.pythonhosted.org/packages/07/49/d4cad6e5381a50947bb973d2f6cf6592621451b09368b8c20d9b8af49c5b/greenlet-3.4.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4df3b0b2289ec686d3c821a5fee44259c05cfe824dd5e6e12c8e5f5df23085cf", size = 665621, upload-time = "2026-04-08T15:56:35.995Z" }, + { url = "https://files.pythonhosted.org/packages/37/31/d1edd54f424761b5d47718822f506b435b6aab2f3f93b465441143ea5119/greenlet-3.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8bff29d586ea415688f4cec96a591fcc3bf762d046a796cdadc1fdb6e7f2d5bf", size = 1622259, upload-time = "2026-04-08T16:26:23.201Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c6/6d3f9cdcb21c4e12a79cb332579f1c6aa1af78eb68059c5a957c7812d95e/greenlet-3.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a569c2fb840c53c13a2b8967c63621fafbd1a0e015b9c82f408c33d626a2fda", size = 1686916, upload-time = "2026-04-08T15:57:34.282Z" }, + { url = "https://files.pythonhosted.org/packages/63/45/c1ca4a1ad975de4727e52d3ffe641ae23e1d7a8ffaa8ff7a0477e1827b92/greenlet-3.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:207ba5b97ea8b0b60eb43ffcacf26969dd83726095161d676aac03ff913ee50d", size = 239821, upload-time = "2026-04-08T17:03:48.423Z" }, + { url = "https://files.pythonhosted.org/packages/71/c4/6f621023364d7e85a4769c014c8982f98053246d142420e0328980933ceb/greenlet-3.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:f8296d4e2b92af34ebde81085a01690f26a51eb9ac09a0fcadb331eb36dbc802", size = 236932, upload-time = "2026-04-08T17:04:33.551Z" }, + { url = "https://files.pythonhosted.org/packages/d4/8f/18d72b629783f5e8d045a76f5325c1e938e659a9e4da79c7dcd10169a48d/greenlet-3.4.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d70012e51df2dbbccfaf63a40aaf9b40c8bed37c3e3a38751c926301ce538ece", size = 294681, upload-time = "2026-04-08T15:52:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ad/5fa86ec46769c4153820d58a04062285b3b9e10ba3d461ee257b68dcbf53/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a58bec0751f43068cd40cff31bb3ca02ad6000b3a51ca81367af4eb5abc480c8", size = 658899, upload-time = "2026-04-08T16:24:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/4e8174ca0e87ae748c409f055a1ba161038c43cc0a5a6f1433a26ac2e5bf/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05fa0803561028f4b2e3b490ee41216a842eaee11aed004cc343a996d9523aa2", size = 665284, upload-time = "2026-04-08T16:31:02.833Z" }, + { url = "https://files.pythonhosted.org/packages/19/da/991cf7cd33662e2df92a1274b7eb4d61769294d38a1bba8a45f31364845e/greenlet-3.4.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e60d38719cb80b3ab5e85f9f1aed4960acfde09868af6762ccb27b260d68f4ed", size = 661861, upload-time = "2026-04-08T15:56:37.269Z" }, + { url = "https://files.pythonhosted.org/packages/36/c5/6c2c708e14db3d9caea4b459d8464f58c32047451142fe2cfd90e7458f41/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f50c804733b43eded05ae694691c9aa68bca7d0a867d67d4a3f514742a2d53f", size = 1622182, upload-time = "2026-04-08T16:26:24.777Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4c/50c5fed19378e11a29fabab1f6be39ea95358f4a0a07e115a51ca93385d8/greenlet-3.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2d4f0635dc4aa638cda4b2f5a07ae9a2cff9280327b581a3fcb6f317b4fbc38a", size = 1685050, upload-time = "2026-04-08T15:57:36.453Z" }, + { url = "https://files.pythonhosted.org/packages/db/72/85ae954d734703ab48e622c59d4ce35d77ce840c265814af9c078cacc7aa/greenlet-3.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1a4a48f24681300c640f143ba7c404270e1ebbbcf34331d7104a4ff40f8ea705", size = 245554, upload-time = "2026-04-08T17:03:50.044Z" }, ] [[package]] @@ -672,62 +672,62 @@ wheels = [ [[package]] name = "librt" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, - { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, - { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, - { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, - { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, - { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, - { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, - { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, - { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, - { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, - { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, - { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, - { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, - { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, - { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, - { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, - { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, - { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, - { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, - { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, - { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, - { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, - { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, - { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, ] [[package]] @@ -904,7 +904,7 @@ wheels = [ [[package]] name = "mypy" -version = "1.20.0" +version = "1.20.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, @@ -912,46 +912,46 @@ dependencies = [ { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, - { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, - { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, - { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, - { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, - { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" }, - { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" }, - { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" }, - { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" }, - { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" }, - { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" }, - { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" }, - { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" }, - { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" }, - { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" }, - { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" }, - { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" }, - { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" }, - { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" }, - { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" }, - { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" }, - { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/0b/3d/5b373635b3146264eb7a68d09e5ca11c305bbb058dfffbb47c47daf4f632/mypy-1.20.1.tar.gz", hash = "sha256:6fc3f4ecd52de81648fed1945498bf42fa2993ddfad67c9056df36ae5757f804", size = 3815892, upload-time = "2026-04-13T02:46:51.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/1b/75a7c825a02781ca10bc2f2f12fba2af5202f6d6005aad8d2d1f264d8d78/mypy-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:36ee2b9c6599c230fea89bbd79f401f9f9f8e9fcf0c777827789b19b7da90f51", size = 14494077, upload-time = "2026-04-13T02:45:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/b0/54/5e5a569ea5c2b4d48b729fb32aa936eeb4246e4fc3e6f5b3d36a2dfbefb9/mypy-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fba3fb0968a7b48806b0c90f38d39296f10766885a94c83bd21399de1e14eb28", size = 13319495, upload-time = "2026-04-13T02:45:29.674Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a4/a1945b19f33e91721b59deee3abb484f2fa5922adc33bb166daf5325d76d/mypy-1.20.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef1415a637cd3627d6304dfbeddbadd21079dafc2a8a753c477ce4fc0c2af54f", size = 13696948, upload-time = "2026-04-13T02:46:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/75e969781c2359b2f9c15b061f28ec6d67c8b61865ceda176e85c8e7f2de/mypy-1.20.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef3461b1ad5cd446e540016e90b5984657edda39f982f4cc45ca317b628f5a37", size = 14706744, upload-time = "2026-04-13T02:46:00.482Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6e/b221b1de981fc4262fe3e0bf9ec272d292dfe42394a689c2d49765c144c4/mypy-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:542dd63c9e1339b6092eb25bd515f3a32a1453aee8c9521d2ddb17dacd840237", size = 14949035, upload-time = "2026-04-13T02:45:06.021Z" }, + { url = "https://files.pythonhosted.org/packages/ca/4b/298ba2de0aafc0da3ff2288da06884aae7ba6489bc247c933f87847c41b3/mypy-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d55c7cd8ca22e31f93af2a01160a9e95465b5878de23dba7e48116052f20a8d", size = 10883216, upload-time = "2026-04-13T02:45:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/c7/f9/5e25b8f0b8cb92f080bfed9c21d3279b2a0b6a601cdca369a039ba84789d/mypy-1.20.1-cp312-cp312-win_arm64.whl", hash = "sha256:f5b84a79070586e0d353ee07b719d9d0a4aa7c8ee90c0ea97747e98cbe193019", size = 9814299, upload-time = "2026-04-13T02:45:21.934Z" }, + { url = "https://files.pythonhosted.org/packages/21/e8/ef0991aa24c8f225df10b034f3c2681213cb54cf247623c6dec9a5744e70/mypy-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f3886c03e40afefd327bd70b3f634b39ea82e87f314edaa4d0cce4b927ddcc1", size = 14500739, upload-time = "2026-04-13T02:46:05.442Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/416ebec3047636ed89fa871dc8c54bf05e9e20aa9499da59790d7adb312d/mypy-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e860eb3904f9764e83bafd70c8250bdffdc7dde6b82f486e8156348bf7ceb184", size = 13314735, upload-time = "2026-04-13T02:46:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/10/1e/1505022d9c9ac2e014a384eb17638fb37bf8e9d0a833ea60605b66f8f7ba/mypy-1.20.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4b5aac6e785719da51a84f5d09e9e843d473170a9045b1ea7ea1af86225df4b", size = 13704356, upload-time = "2026-04-13T02:45:19.773Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/275b01f5eba5c467a3318ec214dd865abb66e9c811231c8587287b92876a/mypy-1.20.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f37b6cd0fe2ad3a20f05ace48ca3523fc52ff86940e34937b439613b6854472e", size = 14696420, upload-time = "2026-04-13T02:45:24.205Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/b3779e134e1b7250d05f874252780d0a88c068bc054bcff99ca20a3a2986/mypy-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4bbb0f6b54ce7cc350ef4a770650d15fa70edd99ad5267e227133eda9c94218", size = 14936093, upload-time = "2026-04-13T02:45:32.087Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/81b64991b0f3f278c3b55c335888794af190b2d59031a5ad1401bcb69f1e/mypy-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:c3dc20f8ec76eecd77148cdd2f1542ed496e51e185713bf488a414f862deb8f2", size = 10889659, upload-time = "2026-04-13T02:46:02.926Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fd/7adcb8053572edf5ef8f3db59599dfeeee3be9cc4c8c97e2d28f66f42ac5/mypy-1.20.1-cp313-cp313-win_arm64.whl", hash = "sha256:a9d62bbac5d6d46718e2b0330b25e6264463ed832722b8f7d4440ff1be3ca895", size = 9815515, upload-time = "2026-04-13T02:46:32.103Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/db831e84c81d57d4886d99feee14e372f64bbec6a9cb1a88a19e243f2ef5/mypy-1.20.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:12927b9c0ed794daedcf1dab055b6c613d9d5659ac511e8d936d96f19c087d12", size = 14483064, upload-time = "2026-04-13T02:45:26.901Z" }, + { url = "https://files.pythonhosted.org/packages/d5/82/74e62e7097fa67da328ac8ece8de09133448c04d20ddeaeba251a3000f01/mypy-1.20.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:752507dd481e958b2c08fc966d3806c962af5a9433b5bf8f3bdd7175c20e34fe", size = 13335694, upload-time = "2026-04-13T02:46:12.514Z" }, + { url = "https://files.pythonhosted.org/packages/74/c4/97e9a0abe4f3cdbbf4d079cb87a03b786efeccf5bf2b89fe4f96939ab2e6/mypy-1.20.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c614655b5a065e56274c6cbbe405f7cf7e96c0654db7ba39bc680238837f7b08", size = 13726365, upload-time = "2026-04-13T02:45:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/d7/aa/a19d884a8d28fcd3c065776323029f204dbc774e70ec9c85eba228b680de/mypy-1.20.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c3f6221a76f34d5100c6d35b3ef6b947054123c3f8d6938a4ba00b1308aa572", size = 14693472, upload-time = "2026-04-13T02:46:41.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/cc9324bd21cf786592b44bf3b5d224b3923c1230ec9898d508d00241d465/mypy-1.20.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4bdfc06303ac06500af71ea0cdbe995c502b3c9ba32f3f8313523c137a25d1b6", size = 14919266, upload-time = "2026-04-13T02:46:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/dc/779abb25a8c63e8f44bf5a336217fa92790fa17e0c40e0c725d10cb01bbd/mypy-1.20.1-cp314-cp314-win_amd64.whl", hash = "sha256:0131edd7eba289973d1ba1003d1a37c426b85cdef76650cd02da6420898a5eb3", size = 11049713, upload-time = "2026-04-13T02:45:57.673Z" }, + { url = "https://files.pythonhosted.org/packages/28/08/4172be2ad7de9119b5a92ca36abbf641afdc5cb1ef4ae0c3a8182f29674f/mypy-1.20.1-cp314-cp314-win_arm64.whl", hash = "sha256:33f02904feb2c07e1fdf7909026206396c9deeb9e6f34d466b4cfedb0aadbbe4", size = 9999819, upload-time = "2026-04-13T02:46:35.039Z" }, + { url = "https://files.pythonhosted.org/packages/2d/af/af9e46b0c8eabbce9fc04a477564170f47a1c22b308822282a59b7ff315f/mypy-1.20.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:168472149dd8cc505c98cefd21ad77e4257ed6022cd5ed2fe2999bed56977a5a", size = 15547508, upload-time = "2026-04-13T02:46:25.588Z" }, + { url = "https://files.pythonhosted.org/packages/a7/cd/39c9e4ad6ba33e069e5837d772a9e6c304b4a5452a14a975d52b36444650/mypy-1.20.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:eb674600309a8f22790cca883a97c90299f948183ebb210fbef6bcee07cb1986", size = 14399557, upload-time = "2026-04-13T02:46:10.021Z" }, + { url = "https://files.pythonhosted.org/packages/83/c1/3fd71bdc118ffc502bf57559c909927bb7e011f327f7bb8e0488e98a5870/mypy-1.20.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef2b2e4cc464ba9795459f2586923abd58a0055487cbe558cb538ea6e6bc142a", size = 15045789, upload-time = "2026-04-13T02:45:10.81Z" }, + { url = "https://files.pythonhosted.org/packages/8e/73/6f07ff8b57a7d7b3e6e5bf34685d17632382395c8bb53364ec331661f83e/mypy-1.20.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee461d396dd46b3f0ed5a098dbc9b8860c81c46ad44fa071afcfbc149f167c9", size = 15850795, upload-time = "2026-04-13T02:45:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e2/f7dffec1c7767078f9e9adf0c786d1fe0ff30964a77eb213c09b8b58cb76/mypy-1.20.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e364926308b3e66f1361f81a566fc1b2f8cd47fc8525e8136d4058a65a4b4f02", size = 16088539, upload-time = "2026-04-13T02:46:17.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/76/e0dee71035316e75a69d73aec2f03c39c21c967b97e277fd0ef8fd6aec66/mypy-1.20.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a0c17fbd746d38c70cbc42647cfd884f845a9708a4b160a8b4f7e70d41f4d7fa", size = 12575567, upload-time = "2026-04-13T02:45:34.795Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/7ed43c9d9c3d1468f86605e323a5d97e411a448790a00f07e779f3211a46/mypy-1.20.1-cp314-cp314t-win_arm64.whl", hash = "sha256:db2cb89654626a912efda69c0d5c1d22d948265e2069010d3dde3abf751c7d08", size = 10378823, upload-time = "2026-04-13T02:45:13.35Z" }, + { url = "https://files.pythonhosted.org/packages/d8/28/926bd972388e65a39ee98e188ccf67e81beb3aacfd5d6b310051772d974b/mypy-1.20.1-py3-none-any.whl", hash = "sha256:1aae28507f253fe82d883790d1c0a0d35798a810117c88184097fe8881052f06", size = 2636553, upload-time = "2026-04-13T02:46:30.45Z" }, ] [[package]] name = "mypy-boto3-s3" -version = "1.42.80" +version = "1.42.85" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/4e/f0f2c0fca253daf2db6230f96899dd0b3b7b592d99f60f07e5b3cc058584/mypy_boto3_s3-1.42.80.tar.gz", hash = "sha256:f3c1d19e8f7346402e341d0395d492b67801f80614bad5d7fa8b5f4db5e4b65d", size = 76540, upload-time = "2026-03-31T19:37:26.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/5e/026461fef8e163ec261df1668ee88611124170bb4da3d1b144c970e7c9b4/mypy_boto3_s3-1.42.85.tar.gz", hash = "sha256:401e3a184ac0973bc08b556cc3b2655d8f2e56570b6ed87ce635210df4f666fb", size = 76543, upload-time = "2026-04-07T19:51:20.608Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/32/aa321332f62f6c8ce174ad3c3edab6235c0ab36c77dae930fea9e41e69bd/mypy_boto3_s3-1.42.80-py3-none-any.whl", hash = "sha256:23d59dee7650c18c62ebbdc96b11bbfd8b1547341f46c88d1aa8d140276a4276", size = 83751, upload-time = "2026-03-31T19:37:22.112Z" }, + { url = "https://files.pythonhosted.org/packages/c0/58/fb6373ca66898620ecb7b9ab92563f3f7277627994bc4ceba75c721de4a1/mypy_boto3_s3-1.42.85-py3-none-any.whl", hash = "sha256:b2cad995ea733b16ae3be5510fd6a0038aa44400c22d010d4def9286cf6eaf82", size = 83751, upload-time = "2026-04-07T19:51:17.727Z" }, ] [[package]] @@ -1197,11 +1197,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.4" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, ] [[package]] @@ -1355,7 +1355,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.2" +version = "9.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1364,9 +1364,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] [[package]] @@ -1395,15 +1395,15 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.2.1" +version = "1.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/88/815e53084c5079a59df912825a279f41dd2e0df82281770eadc732f5352c/python_discovery-1.2.1.tar.gz", hash = "sha256:180c4d114bff1c32462537eac5d6a332b768242b76b69c0259c7d14b1b680c9e", size = 58457, upload-time = "2026-03-26T22:30:44.496Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/0f/019d3949a40280f6193b62bc010177d4ce702d0fce424322286488569cd3/python_discovery-1.2.1-py3-none-any.whl", hash = "sha256:b6a957b24c1cd79252484d3566d1b49527581d46e789aaf43181005e56201502", size = 31674, upload-time = "2026-03-26T22:30:43.396Z" }, + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, ] [[package]] @@ -1492,40 +1492,40 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +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 = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] name = "ruff" -version = "0.15.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, - { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, - { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, - { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, - { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, - { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, - { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, - { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, - { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, - { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, - { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, - { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, - { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, +version = "0.15.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" }, + { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" }, + { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" }, + { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" }, + { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" }, + { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" }, + { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" }, + { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" }, + { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" }, + { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" }, + { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" }, + { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" }, ] [[package]] @@ -1792,7 +1792,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.2.0" +version = "21.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -1800,9 +1800,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/8c/bdd9f89f89e4a787ac61bb2da4d884bc45e0c287ec694dfa3170dddd5cfe/virtualenv-21.2.3.tar.gz", hash = "sha256:9bb6d1414ab55ca624371e30c7719c32f183ef44da544ef8aa44a456de7ac191", size = 5844776, upload-time = "2026-04-14T01:10:36.692Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" }, + { url = "https://files.pythonhosted.org/packages/95/19/bc7c4e05f42532863cf2ae7e7e847beab25835934e0410160b47eeff1e35/virtualenv-21.2.3-py3-none-any.whl", hash = "sha256:486652347ea8526d91e9807c0274583cb7ba31dd4942ff10fb5621402f0fe0d8", size = 5828329, upload-time = "2026-04-14T01:10:34.809Z" }, ] [[package]]