diff --git a/README.md b/README.md index 8a9240f..83c52de 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ The library's building blocks are `ErrorMechanism`s, `ErrorType`s, and `ErrorMod - An `ErrorType` describes _how_ the value is wrong: a typo, an outlier, a category swap, and so on. Read the documentation for a [full list of supported error types](https://tab-err.readthedocs.io/latest/api/tab_err/error_type/index.html). - An `ErrorModel` is a set of mechanisms and types to perturb existing data with realistic errors. It is shareable as metadata. -`tab_err` is supported by a `pandas` backend. +`tab_err` supports pandas and Polars backends, and has experimental support for cuDF, Modin, and PyArrow. ## Examples diff --git a/examples/Error_Types.ipynb b/examples/Error_Types.ipynb index ebb87c9..c84671b 100644 --- a/examples/Error_Types.ipynb +++ b/examples/Error_Types.ipynb @@ -172,10 +172,10 @@ " 3.0\n", " Alice\n", " 1984\n", - " 3.0\n", - " False\n", + " 3.1\n", " False\n", " False\n", + " True\n", " \n", " \n", " 2\n", @@ -184,10 +184,10 @@ " 3.0\n", " Alice\n", " Pride and Prejudice\n", - " 3.1\n", + " 3.0\n", + " False\n", " False\n", " False\n", - " True\n", " \n", " \n", " 3\n", @@ -242,8 +242,8 @@ " error_mask \n", " rating typist book_title rating \n", "0 1.0 False False False \n", - "1 3.0 False False False \n", - "2 3.1 False False True \n", + "1 3.1 False False True \n", + "2 3.0 False False False \n", "3 4.0 False False False \n", "4 2.0 False False False \n", "5 1.0 False False False " @@ -322,10 +322,10 @@ " 1.0\n", " Alice\n", " To Kill a Mockingbird\n", - " 1.0\n", - " False\n", + " 1.1\n", " False\n", " False\n", + " True\n", " \n", " \n", " 1\n", @@ -358,10 +358,10 @@ " 4.0\n", " Bob\n", " The Great Gatsby\n", - " 4.1\n", + " 4.0\n", + " False\n", " False\n", " False\n", - " True\n", " \n", " \n", " 4\n", @@ -403,10 +403,10 @@ "\n", " error_mask \n", " rating typist book_title rating \n", - "0 1.0 False False False \n", + "0 1.1 False False True \n", "1 3.0 False False False \n", "2 3.0 False False False \n", - "3 4.1 False False True \n", + "3 4.0 False False False \n", "4 2.0 False False False \n", "5 1.0 False False False " ] @@ -524,10 +524,10 @@ " 4.0\n", " Bob\n", " The Great Gatsby\n", - " NaN\n", + " 4.0\n", + " False\n", " False\n", " False\n", - " True\n", " \n", " \n", " 4\n", @@ -536,10 +536,10 @@ " 2.0\n", " Bob\n", " Moby-Dick\n", - " 2.0\n", - " False\n", + " NaN\n", " False\n", " False\n", + " True\n", " \n", " \n", " 5\n", @@ -572,8 +572,8 @@ "0 1.0 False False False \n", "1 3.0 False False False \n", "2 3.0 False False False \n", - "3 NaN False False True \n", - "4 2.0 False False False \n", + "3 4.0 False False False \n", + "4 NaN False False True \n", "5 1.0 False False False " ] }, @@ -825,11 +825,11 @@ " 1\n", " Alice\n", " ¿Cómo estás?\n", - " 11/10 12 a.m.\n", + " 12 a.m.\n", + " False\n", " False\n", " False\n", " False\n", - " True\n", " \n", " \n", " 1\n", @@ -840,11 +840,11 @@ " 1\n", " Alice\n", " Привет, как дела?\n", - " 11/10 3 p.m.\n", + " 3 p.m.\n", + " False\n", " False\n", " False\n", " False\n", - " True\n", " \n", " \n", " 2\n", @@ -855,11 +855,11 @@ " 2\n", " Bob\n", " 今日はどうですか\n", - " 3 p.m.\n", - " False\n", + " 11/10 3 p.m.\n", " False\n", " False\n", " False\n", + " True\n", " \n", " \n", " 3\n", @@ -870,11 +870,11 @@ " 2\n", " Bob\n", " Ça va bien, merci.\n", - " 4 a.m.\n", - " False\n", + " 11/10 4 a.m.\n", " False\n", " False\n", " False\n", + " True\n", " \n", " \n", " 4\n", @@ -920,14 +920,14 @@ "4 3 Clara ¡Nos vemos mañana! 1 p.m. 3 Clara \n", "5 4 David Ich hätte Hunger. 1 p.m. 4 David \n", "\n", - " error_mask \n", - " content timestamp user_id user content timestamp \n", - "0 ¿Cómo estás? 11/10 12 a.m. False False False True \n", - "1 Привет, как дела? 11/10 3 p.m. False False False True \n", - "2 今日はどうですか 3 p.m. False False False False \n", - "3 Ça va bien, merci. 4 a.m. False False False False \n", - "4 ¡Nos vemos mañana! 1 p.m. False False False False \n", - "5 Ich hätte Hunger. 11/10 1 p.m. False False False True " + " error_mask \n", + " content timestamp user_id user content timestamp \n", + "0 ¿Cómo estás? 12 a.m. False False False False \n", + "1 Привет, как дела? 3 p.m. False False False False \n", + "2 今日はどうですか 11/10 3 p.m. False False False True \n", + "3 Ça va bien, merci. 11/10 4 a.m. False False False True \n", + "4 ¡Nos vemos mañana! 1 p.m. False False False False \n", + "5 Ich hätte Hunger. 11/10 1 p.m. False False False True " ] }, "execution_count": 8, @@ -1008,11 +1008,11 @@ " Alice\n", " ¿Cómo estás?\n", " 12 a.m.\n", - " 1\n", + " 1.0\n", " Alice\n", " ¿Cómo estás?\n", " 12 a.m.\n", - " False\n", + " True\n", " False\n", " False\n", " False\n", @@ -1023,7 +1023,7 @@ " Alice\n", " Привет, как дела?\n", " 3 p.m.\n", - " 1\n", + " 1.0\n", " Alice\n", " Привет, как дела?\n", " 3 p.m.\n", @@ -1053,7 +1053,7 @@ " Bob\n", " Ça va bien, merci.\n", " 4 a.m.\n", - " 2\n", + " 2.0\n", " Bob\n", " Ça va bien, merci.\n", " 4 a.m.\n", @@ -1072,7 +1072,7 @@ " Clara\n", " ¡Nos vemos mañana!\n", " 1 p.m.\n", - " True\n", + " False\n", " False\n", " False\n", " False\n", @@ -1099,20 +1099,20 @@ "text/plain": [ " original perturbed \\\n", " user_id user content timestamp user_id user \n", - "0 1 Alice ¿Cómo estás? 12 a.m. 1 Alice \n", - "1 1 Alice Привет, как дела? 3 p.m. 1 Alice \n", + "0 1 Alice ¿Cómo estás? 12 a.m. 1.0 Alice \n", + "1 1 Alice Привет, как дела? 3 p.m. 1.0 Alice \n", "2 2 Bob 今日はどうですか 3 p.m. 2.0 Bob \n", - "3 2 Bob Ça va bien, merci. 4 a.m. 2 Bob \n", + "3 2 Bob Ça va bien, merci. 4 a.m. 2.0 Bob \n", "4 3 Clara ¡Nos vemos mañana! 1 p.m. 3.0 Clara \n", "5 4 David Ich hätte Hunger. 1 p.m. 4.0 David \n", "\n", " error_mask \n", " content timestamp user_id user content timestamp \n", - "0 ¿Cómo estás? 12 a.m. False False False False \n", + "0 ¿Cómo estás? 12 a.m. True False False False \n", "1 Привет, как дела? 3 p.m. False False False False \n", "2 今日はどうですか 3 p.m. True False False False \n", "3 Ça va bien, merci. 4 a.m. False False False False \n", - "4 ¡Nos vemos mañana! 1 p.m. True False False False \n", + "4 ¡Nos vemos mañana! 1 p.m. False False False False \n", "5 Ich hätte Hunger. 1 p.m. True False False False " ] }, @@ -1134,9 +1134,9 @@ "id": "50e77720-d85c-4154-8fed-9698c22d2057", "metadata": {}, "source": [ - "## [Mojibake](https://en.wikipedia.org/wiki/Mojibake)\n", + "## Mojibake\n", "\n", - "Mojibake occurs if the sender and receiver do not use the same text encoding." + "[Mojibake](https://en.wikipedia.org/wiki/Mojibake) occurs if the sender and receiver do not use the same text encoding." ] }, { @@ -1210,7 +1210,7 @@ " 3 p.m.\n", " 1\n", " Alice\n", - " Ďđčâĺň, ęŕę äĺëŕ?\n", + " ŹąŹâŹÚŹÓŹÖŹä, ŹÜŹŃŹÜ ŹŐŹÖŹÝŹŃ?\n", " 3 p.m.\n", " False\n", " False\n", @@ -1225,11 +1225,11 @@ " 3 p.m.\n", " 2\n", " Bob\n", - " 今日はどうですか\n", + " ĐŃěíŞĎŞÉŞŚŞÇŞšŞŤ\n", " 3 p.m.\n", " False\n", " False\n", - " False\n", + " True\n", " False\n", " \n", " \n", @@ -1240,11 +1240,11 @@ " 4 a.m.\n", " 2\n", " Bob\n", - " a va bien, merci.\n", + " Ça va bien, merci.\n", " 4 a.m.\n", " False\n", " False\n", - " True\n", + " False\n", " False\n", " \n", " \n", @@ -1255,11 +1255,11 @@ " 1 p.m.\n", " 3\n", " Clara\n", - " ¡Nos vemos mañana!\n", + " ˘ŽNos vemos maana!\n", " 1 p.m.\n", " False\n", " False\n", - " False\n", + " True\n", " False\n", " \n", " \n", @@ -1270,11 +1270,11 @@ " 1 p.m.\n", " 4\n", " David\n", - " Ich htte Hunger.\n", + " Ich hätte Hunger.\n", " 1 p.m.\n", " False\n", " False\n", - " True\n", + " False\n", " False\n", " \n", " \n", @@ -1291,14 +1291,23 @@ "4 3 Clara ¡Nos vemos mañana! 1 p.m. 3 Clara \n", "5 4 David Ich hätte Hunger. 1 p.m. 4 David \n", "\n", - " error_mask \n", - " content timestamp user_id user content timestamp \n", - "0 ¿Cómo estás? 12 a.m. False False False False \n", - "1 Ďđčâĺň, ęŕę äĺëŕ? 3 p.m. False False True False \n", - "2 今日はどうですか 3 p.m. False False False False \n", - "3 a va bien, merci. 4 a.m. False False True False \n", - "4 ¡Nos vemos mañana! 1 p.m. False False False False \n", - "5 Ich htte Hunger. 1 p.m. False False True False " + " error_mask \\\n", + " content timestamp user_id user content \n", + "0 ¿Cómo estás? 12 a.m. False False False \n", + "1 ŹąŹâŹÚŹÓŹÖŹä, ŹÜŹŃŹÜ ŹŐŹÖŹÝŹŃ? 3 p.m. False False True \n", + "2 ĐŃěíŞĎŞÉŞŚŞÇŞšŞŤ 3 p.m. False False True \n", + "3 Ça va bien, merci. 4 a.m. False False False \n", + "4 ˘ŽNos vemos maana! 1 p.m. False False True \n", + "5 Ich hätte Hunger. 1 p.m. False False False \n", + "\n", + " \n", + " timestamp \n", + "0 False \n", + "1 False \n", + "2 False \n", + "3 False \n", + "4 False \n", + "5 False " ] }, "execution_count": 10, @@ -1376,7 +1385,7 @@ " 1.0\n", " Alice\n", " To Kill a Mockingbird\n", - " -2.778344\n", + " -2.705336\n", " False\n", " False\n", " True\n", @@ -1388,7 +1397,7 @@ " 3.0\n", " Alice\n", " 1984\n", - " 6.235905\n", + " 6.406944\n", " False\n", " False\n", " True\n", @@ -1400,10 +1409,10 @@ " 3.0\n", " Alice\n", " Pride and Prejudice\n", - " 3.000000\n", - " False\n", + " 6.181281\n", " False\n", " False\n", + " True\n", " \n", " \n", " 3\n", @@ -1424,10 +1433,10 @@ " 2.0\n", " Bob\n", " Moby-Dick\n", - " -1.759203\n", + " 2.000000\n", + " False\n", " False\n", " False\n", - " True\n", " \n", " \n", " 5\n", @@ -1457,11 +1466,11 @@ "\n", " error_mask \n", " rating typist book_title rating \n", - "0 -2.778344 False False True \n", - "1 6.235905 False False True \n", - "2 3.000000 False False False \n", + "0 -2.705336 False False True \n", + "1 6.406944 False False True \n", + "2 6.181281 False False True \n", "3 4.000000 False False False \n", - "4 -1.759203 False False True \n", + "4 2.000000 False False False \n", "5 1.000000 False False False " ] }, @@ -1530,14 +1539,14 @@ " \n", " 0\n", " service-A-2024-02-01\n", - " service-A-2024-02-01\n", - " False\n", + " 01-A-02-service-2024\n", + " True\n", " \n", " \n", " 1\n", " service-A-2024-02-02\n", - " service-A-2024-02-02\n", - " False\n", + " 02-2024-service-02-A\n", + " True\n", " \n", " \n", " 2\n", @@ -1548,14 +1557,14 @@ " \n", " 3\n", " service-A-2024-02-01\n", - " 01-A-2024-service-02\n", + " A-service-01-2024-02\n", " True\n", " \n", " \n", " 4\n", " service-B-2024-02-02\n", - " B-service-2024-02-02\n", - " True\n", + " service-B-2024-02-02\n", + " False\n", " \n", " \n", " 5\n", @@ -1566,19 +1575,19 @@ " \n", " 6\n", " service-C-2024-02-01\n", - " 01-02-service-C-2024\n", - " True\n", + " service-C-2024-02-01\n", + " False\n", " \n", " \n", " 7\n", " service-C-2024-02-02\n", - " C-service-02-2024-02\n", + " service-C-02-2024-02\n", " True\n", " \n", " \n", " 8\n", " service-C-2024-02-03\n", - " 03-C-service-02-2024\n", + " C-service-02-03-2024\n", " True\n", " \n", " \n", @@ -1588,15 +1597,15 @@ "text/plain": [ " original perturbed error_mask\n", " service service service\n", - "0 service-A-2024-02-01 service-A-2024-02-01 False\n", - "1 service-A-2024-02-02 service-A-2024-02-02 False\n", + "0 service-A-2024-02-01 01-A-02-service-2024 True\n", + "1 service-A-2024-02-02 02-2024-service-02-A True\n", "2 service-A-2024-02-03 service-A-2024-02-03 False\n", - "3 service-A-2024-02-01 01-A-2024-service-02 True\n", - "4 service-B-2024-02-02 B-service-2024-02-02 True\n", + "3 service-A-2024-02-01 A-service-01-2024-02 True\n", + "4 service-B-2024-02-02 service-B-2024-02-02 False\n", "5 service-B-2024-02-03 service-B-2024-02-03 False\n", - "6 service-C-2024-02-01 01-02-service-C-2024 True\n", - "7 service-C-2024-02-02 C-service-02-2024-02 True\n", - "8 service-C-2024-02-03 03-C-service-02-2024 True" + "6 service-C-2024-02-01 service-C-2024-02-01 False\n", + "7 service-C-2024-02-02 service-C-02-2024-02 True\n", + "8 service-C-2024-02-03 C-service-02-03-2024 True" ] }, "execution_count": 12, @@ -1662,26 +1671,26 @@ " \n", " 0\n", " service-A-2024-02-01\n", - " service-01-A-02-2024\n", - " True\n", + " service-A-2024-02-01\n", + " False\n", " \n", " \n", " 1\n", " service-A-2024-02-02\n", - " service-A-2024-02-02\n", - " False\n", + " service-02-02-A-2024\n", + " True\n", " \n", " \n", " 2\n", " service-A-2024-02-03\n", - " service-03-A-02-2024\n", + " service-02-03-A-2024\n", " True\n", " \n", " \n", " 3\n", " service-A-2024-02-01\n", - " service-01-A-02-2024\n", - " True\n", + " service-A-2024-02-01\n", + " False\n", " \n", " \n", " 4\n", @@ -1692,26 +1701,26 @@ " \n", " 5\n", " service-B-2024-02-03\n", - " service-B-2024-02-03\n", - " False\n", + " service-02-03-B-2024\n", + " True\n", " \n", " \n", " 6\n", " service-C-2024-02-01\n", - " service-01-C-02-2024\n", - " True\n", + " service-C-2024-02-01\n", + " False\n", " \n", " \n", " 7\n", " service-C-2024-02-02\n", - " service-02-C-02-2024\n", + " service-02-02-C-2024\n", " True\n", " \n", " \n", " 8\n", " service-C-2024-02-03\n", - " service-C-2024-02-03\n", - " False\n", + " service-02-03-C-2024\n", + " True\n", " \n", " \n", "\n", @@ -1720,15 +1729,15 @@ "text/plain": [ " original perturbed error_mask\n", " service service service\n", - "0 service-A-2024-02-01 service-01-A-02-2024 True\n", - "1 service-A-2024-02-02 service-A-2024-02-02 False\n", - "2 service-A-2024-02-03 service-03-A-02-2024 True\n", - "3 service-A-2024-02-01 service-01-A-02-2024 True\n", + "0 service-A-2024-02-01 service-A-2024-02-01 False\n", + "1 service-A-2024-02-02 service-02-02-A-2024 True\n", + "2 service-A-2024-02-03 service-02-03-A-2024 True\n", + "3 service-A-2024-02-01 service-A-2024-02-01 False\n", "4 service-B-2024-02-02 service-B-2024-02-02 False\n", - "5 service-B-2024-02-03 service-B-2024-02-03 False\n", - "6 service-C-2024-02-01 service-01-C-02-2024 True\n", - "7 service-C-2024-02-02 service-02-C-02-2024 True\n", - "8 service-C-2024-02-03 service-C-2024-02-03 False" + "5 service-B-2024-02-03 service-02-03-B-2024 True\n", + "6 service-C-2024-02-01 service-C-2024-02-01 False\n", + "7 service-C-2024-02-02 service-02-02-C-2024 True\n", + "8 service-C-2024-02-03 service-02-03-C-2024 True" ] }, "execution_count": 13, @@ -1796,32 +1805,32 @@ " \n", " 0\n", " service-A-2024-02-01\n", - " service_A_2024_02_01\n", - " True\n", + " service-A-2024-02-01\n", + " False\n", " \n", " \n", " 1\n", " service-A-2024-02-02\n", - " service-A-2024-02-02\n", - " False\n", + " service_A_2024_02_02\n", + " True\n", " \n", " \n", " 2\n", " service-A-2024-02-03\n", - " service_A_2024_02_03\n", - " True\n", + " service-A-2024-02-03\n", + " False\n", " \n", " \n", " 3\n", " service-A-2024-02-01\n", - " service-A-2024-02-01\n", - " False\n", + " service_A_2024_02_01\n", + " True\n", " \n", " \n", " 4\n", " service-B-2024-02-02\n", - " service_B_2024_02_02\n", - " True\n", + " service-B-2024-02-02\n", + " False\n", " \n", " \n", " 5\n", @@ -1832,20 +1841,20 @@ " \n", " 6\n", " service-C-2024-02-01\n", - " service_C_2024_02_01\n", - " True\n", + " service-C-2024-02-01\n", + " False\n", " \n", " \n", " 7\n", " service-C-2024-02-02\n", - " service-C-2024-02-02\n", - " False\n", + " service_C_2024_02_02\n", + " True\n", " \n", " \n", " 8\n", " service-C-2024-02-03\n", - " service-C-2024-02-03\n", - " False\n", + " service_C_2024_02_03\n", + " True\n", " \n", " \n", "\n", @@ -1854,15 +1863,15 @@ "text/plain": [ " original perturbed error_mask\n", " service service service\n", - "0 service-A-2024-02-01 service_A_2024_02_01 True\n", - "1 service-A-2024-02-02 service-A-2024-02-02 False\n", - "2 service-A-2024-02-03 service_A_2024_02_03 True\n", - "3 service-A-2024-02-01 service-A-2024-02-01 False\n", - "4 service-B-2024-02-02 service_B_2024_02_02 True\n", + "0 service-A-2024-02-01 service-A-2024-02-01 False\n", + "1 service-A-2024-02-02 service_A_2024_02_02 True\n", + "2 service-A-2024-02-03 service-A-2024-02-03 False\n", + "3 service-A-2024-02-01 service_A_2024_02_01 True\n", + "4 service-B-2024-02-02 service-B-2024-02-02 False\n", "5 service-B-2024-02-03 service_B_2024_02_03 True\n", - "6 service-C-2024-02-01 service_C_2024_02_01 True\n", - "7 service-C-2024-02-02 service-C-2024-02-02 False\n", - "8 service-C-2024-02-03 service-C-2024-02-03 False" + "6 service-C-2024-02-01 service-C-2024-02-01 False\n", + "7 service-C-2024-02-02 service_C_2024_02_02 True\n", + "8 service-C-2024-02-03 service_C_2024_02_03 True" ] }, "execution_count": 14, @@ -1939,7 +1948,7 @@ " To Kill a Mockingbird\n", " 1.0\n", " Alice\n", - " To Kill a Mockingburd\n", + " Fo Kill a Mockingbird\n", " 1.0\n", " False\n", " True\n", @@ -1963,7 +1972,7 @@ " Pride and Prejudice\n", " 3.0\n", " Alice\n", - " Pride abd Prejudice\n", + " Lride and Prejudice\n", " 3.0\n", " False\n", " True\n", @@ -1975,10 +1984,10 @@ " The Great Gatsby\n", " 4.0\n", " Bob\n", - " The Great Gatsby\n", + " Rhe Great Gatsby\n", " 4.0\n", " False\n", - " False\n", + " True\n", " False\n", " \n", " \n", @@ -1999,10 +2008,10 @@ " The Catcher in the Rye\n", " 1.0\n", " Bob\n", - " The Catcher 9n the Rye\n", + " The Catcher in the Rye\n", " 1.0\n", " False\n", - " True\n", + " False\n", " False\n", " \n", " \n", @@ -2012,21 +2021,21 @@ "text/plain": [ " original perturbed \\\n", " typist book_title rating typist book_title \n", - "0 Alice To Kill a Mockingbird 1.0 Alice To Kill a Mockingburd \n", + "0 Alice To Kill a Mockingbird 1.0 Alice Fo Kill a Mockingbird \n", "1 Alice 1984 3.0 Alice 1984 \n", - "2 Alice Pride and Prejudice 3.0 Alice Pride abd Prejudice \n", - "3 Bob The Great Gatsby 4.0 Bob The Great Gatsby \n", + "2 Alice Pride and Prejudice 3.0 Alice Lride and Prejudice \n", + "3 Bob The Great Gatsby 4.0 Bob Rhe Great Gatsby \n", "4 Bob Moby-Dick 2.0 Bob Moby-Dick \n", - "5 Bob The Catcher in the Rye 1.0 Bob The Catcher 9n the Rye \n", + "5 Bob The Catcher in the Rye 1.0 Bob The Catcher in the Rye \n", "\n", " error_mask \n", " rating typist book_title rating \n", "0 1.0 False True False \n", "1 3.0 False False False \n", "2 3.0 False True False \n", - "3 4.0 False False False \n", + "3 4.0 False True False \n", "4 2.0 False False False \n", - "5 1.0 False True False " + "5 1.0 False False False " ] }, "execution_count": 15, @@ -2128,10 +2137,10 @@ " 3.0\n", " Alice\n", " Pride and Prejudice\n", - " 30.0\n", + " 3.0\n", + " False\n", " False\n", " False\n", - " True\n", " \n", " \n", " 3\n", @@ -2140,10 +2149,10 @@ " 4.0\n", " Bob\n", " The Great Gatsby\n", - " 4.0\n", - " False\n", + " 40.0\n", " False\n", " False\n", + " True\n", " \n", " \n", " 4\n", @@ -2187,8 +2196,8 @@ " rating typist book_title rating \n", "0 10.0 False False True \n", "1 3.0 False False False \n", - "2 30.0 False False True \n", - "3 4.0 False False False \n", + "2 3.0 False False False \n", + "3 40.0 False False True \n", "4 20.0 False False True \n", "5 1.0 False False False " ] @@ -2223,7 +2232,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.19" + "version": "3.13.5" } }, "nbformat": 4, diff --git a/pyproject.toml b/pyproject.toml index d983f89..4e2f1ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,8 +26,8 @@ classifiers = [ requires-python = ">=3.10,<3.15" dependencies = [ - "numpy>=2.2.6,<2.5.0", - "pandas>=2.3.3,<2.4.0", + "narwhals>=1.30.0", + "numpy>=1.24.0", ] [project.urls] @@ -41,6 +41,9 @@ ci = [ "tomli>=2.2.1", ] dev = [ + "pandas>=2.0.0", + "polars>=1.0.0", + "pyarrow>=14.0.0", # Required for polars.to_pandas() "pytest>=8.3.5,<9.0.0", "ruff>=0.9.8", "pre-commit>=4.0.1,<5.0.0", @@ -98,3 +101,8 @@ convention = "google" [tool.ty.environment] python-version = "3.10" + +[tool.pytest.ini_options] +filterwarnings = [ + "ignore::UserWarning" +] diff --git a/tab_err/_error_model.py b/tab_err/_error_model.py index 4908e1c..b74179e 100644 --- a/tab_err/_error_model.py +++ b/tab_err/_error_model.py @@ -6,7 +6,7 @@ from tab_err.api import low_level if TYPE_CHECKING: - import pandas as pd + from narwhals.typing import IntoDataFrame from tab_err import ErrorMechanism, ErrorType @@ -25,17 +25,18 @@ class ErrorModel: error_type: ErrorType error_rate: float - def apply(self: ErrorModel, data: pd.DataFrame, column: str | int) -> tuple[pd.DataFrame, pd.DataFrame]: - """Applies the defined ErrorModel to the given column of a pandas DataFrame. + def apply(self: ErrorModel, data: IntoDataFrame, column: str | int) -> tuple[IntoDataFrame, IntoDataFrame]: + """Applies the defined ErrorModel to the given column of a DataFrame. Args: - data (pd.DataFrame): The pandas DataFrame to create errors in. + data (IntoDataFrame): The DataFrame to create errors in. Supports pandas, Polars, and other narwhals-compatible backends. column (str | int): The column to create errors in. Returns: - tuple[pd.DataFrame, pd.DataFrame]: + tuple[IntoDataFrame, IntoDataFrame]: - The first element is a copy of 'data' with errors. - The second element is the associated error mask. + Both are returned in the same format as the input data. """ data_with_errors, error_mask = low_level.create_errors( data=data, column=column, error_rate=self.error_rate, error_mechanism=self.error_mechanism, error_type=self.error_type diff --git a/tab_err/_utils.py b/tab_err/_utils.py index 629d274..5061139 100644 --- a/tab_err/_utils.py +++ b/tab_err/_utils.py @@ -1,26 +1,30 @@ from __future__ import annotations -import random from typing import TYPE_CHECKING -import numpy as np - if TYPE_CHECKING: - import pandas as pd + from collections.abc import Sequence + from narwhals.typing import IntoDType + +import random +import warnings +from typing import Any -def set_column(data: pd.DataFrame, column: int | str, series: pd.Series) -> None: +import narwhals as nw +import numpy as np + + +def set_column(data: nw.DataFrame, column: int | str, series: nw.Series) -> nw.DataFrame: """Replaces a column in the given DataFrame with the given Series. - Mutates data and changes the dtype of the original data to that of the series, - which, depending on the error type, might change. + Returns a new DataFrame with the column replaced. """ - col = data.columns[column] if isinstance(column, int) else column - data[col] = data[col].astype(series.dtype) - data[col] = series + col_name = get_column_str(data, column) + return data.with_columns(series.alias(col_name)) -def get_column_str(data: pd.DataFrame, column: int | str) -> str: +def get_column_str(data: nw.DataFrame, column: int | str) -> str: """Return column's name of the given DataFrame, where column can be defined as name or index.""" if isinstance(column, int): col = data.columns[column] @@ -33,7 +37,7 @@ def get_column_str(data: pd.DataFrame, column: int | str) -> str: return col -def get_column(data: pd.DataFrame, column: int | str) -> pd.Series: +def get_column(data: nw.DataFrame, column: int | str) -> nw.Series: """Selects a column from the given DataFrame and returns it as a Series.""" return data[get_column_str(data, column)] @@ -56,8 +60,87 @@ def check_error_rate(error_rate: float) -> None: raise ValueError(msg) -def check_data_emptiness(data: pd.DataFrame) -> None: +def check_data_emptiness(data: nw.DataFrame) -> None: """Check that the dataset is not empty, raise a ValueError otherwise.""" - if data.empty: + if data.is_empty(): msg = "The dataframe is empty, cannot introduce errors." raise ValueError(msg) + + +def is_string_dtype(series: nw.Series) -> bool: + """Check if a series has a string dtype.""" + return series.dtype in {nw.String, nw.Object} + + +def is_numeric_dtype(series: nw.Series) -> bool: + """Check if a series has a numeric dtype.""" + return series.dtype.is_numeric() + + +def is_integer_dtype(series: nw.Series) -> bool: + """Check if a series has an integer dtype.""" + return series.dtype.is_integer() + + +def is_datetime_dtype(series: nw.Series) -> bool: + """Check if a series has a datetime dtype.""" + return series.dtype == nw.Datetime + + +def select_string_columns(data: nw.DataFrame) -> list[str | int]: + """Select columns with string dtype.""" + return [col for col in data.columns if is_string_dtype(data[col])] + + +def select_numeric_columns(data: nw.DataFrame) -> list[str | int]: + """Select columns with numeric dtype.""" + return [col for col in data.columns if is_numeric_dtype(data[col])] + + +def select_datetime_columns(data: nw.DataFrame) -> list[str | int]: + """Select columns with datetime dtype.""" + return [col for col in data.columns if is_datetime_dtype(data[col])] + + +def select_numeric_or_datetime_columns(data: nw.DataFrame) -> list[str | int]: + """Select columns with numeric or datetime dtype.""" + return [col for col in data.columns if is_numeric_dtype(data[col]) or is_datetime_dtype(data[col])] + + +def create_empty_boolean_mask(data: nw.DataFrame) -> nw.DataFrame: + """Create an empty boolean mask DataFrame with the same shape as data.""" + n_rows = len(data) + mask_values = [False] * n_rows + return nw.from_dict( + dict.fromkeys(data.columns, mask_values), + backend=nw.get_native_namespace(data), + ) + + +def cast_series_like(series: nw.Series, like: nw.Series, column: int | str) -> nw.Series: + """Cast series to the dtype of 'like' when possible, otherwise keep original.""" + if series.dtype == like.dtype: + return series + dtype: IntoDType = like.dtype + + try: + return series.cast(dtype) + except Exception as exc: # noqa: BLE001 + msg = f"Failed to cast column {column} to dtype {like.dtype}: {exc}. Keeping inferred dtype." + warnings.warn(msg, stacklevel=2) + return series + + +def _values_to_list(values: Sequence[Any] | np.ndarray) -> list[Any]: + """Normalize values into a list for nw.new_series.""" + if isinstance(values, np.ndarray): + return values.tolist() + return list(values) + + +def new_series_like(data: nw.DataFrame, column: int | str, values: Sequence[Any] | np.ndarray) -> nw.Series: + """Create a new series for 'column' and cast it back to the original dtype.""" + col_name = get_column_str(data, column) + original = get_column(data, column) + series = nw.new_series(col_name, _values_to_list(values), backend=nw.get_native_namespace(data)) + return cast_series_like(series, original, column) diff --git a/tab_err/api/high_level.py b/tab_err/api/high_level.py index 7570981..b83d219 100644 --- a/tab_err/api/high_level.py +++ b/tab_err/api/high_level.py @@ -3,7 +3,7 @@ import warnings from typing import TYPE_CHECKING -import pandas as pd +import narwhals as nw from tab_err import ErrorMechanism, ErrorType, error_mechanism, error_type from tab_err._error_model import ErrorModel @@ -11,6 +11,7 @@ from tab_err.api import MidLevelConfig, mid_level if TYPE_CHECKING: + from narwhals.typing import IntoDataFrame from numpy.random import Generator @@ -39,7 +40,7 @@ def _are_same_error_mechanism(error_mechanism1: ErrorMechanism, error_mechanism2 def _build_column_type_dictionary( - data: pd.DataFrame, + data: nw.DataFrame, random_generator: Generator, error_types_to_include: list[ErrorType] | None = None, error_types_to_exclude: list[ErrorType] | None = None, @@ -47,7 +48,7 @@ def _build_column_type_dictionary( """Creates a dictionary mapping from column names to the list of valid error types to apply to that column. Args: - data (pd.DataFrame): The pandas DataFrame to create errors in. + data (nw.DataFrame): The DataFrame to create errors in. random_generator (Generator): Random Generator. Defaults to None. error_types_to_include (list[ErrorType] | None, optional): A list of the error types to be included when building error models. Defaults to None. error_types_to_exclude (list[ErrorType] | None, optional): A list of the error types to be excluded when building error models. Defaults to None. @@ -92,7 +93,7 @@ def _build_column_type_dictionary( # else: do nothing because the default behavior uses all error types if len(error_types_applied) == 0: - msg = "The list of error types to be applied cannot have length 0. Use the default or resturcture your input." + msg = "The list of error types to be applied cannot have length 0. Use the default or restructure your input." raise ValueError(msg) return { @@ -101,7 +102,7 @@ def _build_column_type_dictionary( def _build_column_mechanism_dictionary( - data: pd.DataFrame, + data: nw.DataFrame, random_generator: Generator, error_mechanisms_to_include: list[ErrorMechanism] | None = None, error_mechanisms_to_exclude: list[ErrorMechanism] | None = None, @@ -109,7 +110,7 @@ def _build_column_mechanism_dictionary( """Builds a dictionary mapping from column names to the list of valid error mechanisms to apply to that column. Args: - data (pd.DataFrame): The pandas DataFrame to create errors in. + data (nw.DataFrame): The DataFrame to create errors in. random_generator (Generator): Random Generator. Defaults to None. error_mechanisms_to_include (list[ErrorMechanism] | None, optional): The error mechanisms (EAR, ECAR, ENAR) to include from the dictionary. Defaults to None. @@ -123,7 +124,7 @@ def _build_column_mechanism_dictionary( msg = "Possible conflict in error mechanisms to apply. Set at least on of: error_mechanisms_to_exclude or error_mechanisms_to_include to None." raise ValueError(msg) - columns_mechanisms = {} + columns_mechanisms: dict[int | str, list[ErrorMechanism]] = {} if error_mechanisms_to_include is not None and error_mechanisms_to_exclude is None: # Include specified if not all(issubclass(type(cls), ErrorMechanism) for cls in error_mechanisms_to_include): # Check input @@ -164,19 +165,19 @@ def _build_column_mechanism_dictionary( def _build_column_number_of_models_dictionary( - data: pd.DataFrame, column_types: dict[int | str, list[ErrorType]], column_mechanisms: dict[int | str, list[ErrorMechanism]] + data: nw.DataFrame, column_types: dict[int | str, list[ErrorType]], column_mechanisms: dict[int | str, list[ErrorMechanism]] ) -> dict[int | str, int]: """Builds a dictionary mapping from column names to the number of error models to apply to that column. Args: - data (pd.DataFrame): The pandas DataFrame to create errors in. + data (nw.DataFrame): The DataFrame to create errors in. column_types (dict[int | str, list[ErrorType]]): A dictionary mapping from column names to the list of valid error types to apply to that column. column_mechanisms (dict[int | str, list[ErrorMechanism]]): A dictionary mapping from column names to the list of valid error mechanisms to apply. Returns: dict[int | str, int]: A dictionary mapping from column names to the number of error models to apply to that column. """ - column_num_models = {} + column_num_models: dict[int | str, int] = {} for column in data.columns: column_num_models[column] = len(column_types[column]) * len(column_mechanisms[column]) @@ -189,7 +190,7 @@ def _build_column_number_of_models_dictionary( def create_errors( # noqa: PLR0913 - data: pd.DataFrame, + data: IntoDataFrame, error_rate: float, n_error_models_per_column: int = 1, error_types_to_include: list[ErrorType] | None = None, @@ -197,11 +198,11 @@ def create_errors( # noqa: PLR0913 error_mechanisms_to_include: list[ErrorMechanism] | None = None, error_mechanisms_to_exclude: list[ErrorMechanism] | None = None, seed: int | None = None, -) -> tuple[pd.DataFrame, pd.DataFrame]: +) -> tuple[IntoDataFrame, IntoDataFrame]: """Creates errors in a given DataFrame, at a rate of *approximately* max_error_rate. Args: - data (pd.DataFrame): The pandas DataFrame to create errors in. + data (IntoDataFrame): The DataFrame to create errors in. Supports pandas, Polars, and (experimental) other narwhals-compatible backends. error_rate (float): The maximum error rate to be introduced to each column in the DataFrame. n_error_models_per_column (int, optional): The number of valid error models to apply to each column. Defaults to 1. error_types_to_include (list[ErrorType] | None, optional): A list of the error types to be included when building error models. Defaults to None. @@ -215,39 +216,43 @@ def create_errors( # noqa: PLR0913 seed (int | None, optional): Random seed. Defaults to None. Returns: - tuple[pd.DataFrame, pd.DataFrame]: + tuple[IntoDataFrame, IntoDataFrame]: - The first element is a copy of 'data' with errors. - The second element is the associated error mask. + Both are returned in the same format as the input data. """ + # Wrap native DataFrame to narwhals + data_nw = nw.from_native(data, eager_only=True) + random_generator = seed_randomness_and_get_generator(seed=seed) # Input Checking check_error_rate(error_rate) - check_data_emptiness(data) + check_data_emptiness(data_nw) # Set Up Data - data_copy = data.copy() - error_mask = pd.DataFrame(data=False, index=data.index, columns=data.columns) + data_copy = data_nw.clone() # Build Dictionaries col_type = _build_column_type_dictionary( - data=data, random_generator=random_generator, error_types_to_include=error_types_to_include, error_types_to_exclude=error_types_to_exclude + data=data_nw, random_generator=random_generator, error_types_to_include=error_types_to_include, error_types_to_exclude=error_types_to_exclude ) col_mechanisms = _build_column_mechanism_dictionary( - data=data, + data=data_nw, random_generator=random_generator, error_mechanisms_to_include=error_mechanisms_to_include, error_mechanisms_to_exclude=error_mechanisms_to_exclude, ) - col_num_models = _build_column_number_of_models_dictionary(data=data, column_types=col_type, column_mechanisms=col_mechanisms) + col_num_models = _build_column_number_of_models_dictionary(data=data_nw, column_types=col_type, column_mechanisms=col_mechanisms) if n_error_models_per_column > 0: error_rate = error_rate / n_error_models_per_column config_dictionary: dict[str | int, list[ErrorModel]] = { - column: [] for column in data.columns if col_num_models[column] > 0 + column: [] for column in data_nw.columns if col_num_models[column] > 0 } # Filter out those columns with no valid error models - if error_rate * len(data) < 1: # This value is calculated and rounded to 0 in the sample function of the error mechanism subclasses "n_errors" - msg = f"With a per-model error rate of: {error_rate} and {len(data)} rows, 0 errors will be introduced." + n_rows = len(data_nw) + if error_rate * n_rows < 1: # This value is calculated and rounded to 0 in the sample function of the error mechanism subclasses "n_errors" + msg = f"With a per-model error rate of: {error_rate} and {n_rows} rows, 0 errors will be introduced." warnings.warn(msg, stacklevel=2) for column, error_model_list in config_dictionary.items(): @@ -264,6 +269,6 @@ def create_errors( # noqa: PLR0913 msg = f"n_error_models_per_column is: {n_error_models_per_column} and should be a positive integer" raise ValueError(msg) - # Create Errors & Return - dirty_data, error_mask = mid_level.create_errors(data_copy, config) - return dirty_data, error_mask + # Create Errors & Return (mid_level handles native conversion) + dirty_data_native, error_mask_native = mid_level.create_errors(nw.to_native(data_copy), config) + return dirty_data_native, error_mask_native diff --git a/tab_err/api/low_level.py b/tab_err/api/low_level.py index e2f6311..8893189 100644 --- a/tab_err/api/low_level.py +++ b/tab_err/api/low_level.py @@ -2,37 +2,46 @@ from typing import TYPE_CHECKING +import narwhals as nw + from tab_err._utils import check_data_emptiness, check_error_rate, set_column if TYPE_CHECKING: - import pandas as pd + from narwhals.typing import IntoDataFrame from tab_err import ErrorMechanism, ErrorType def create_errors( - data: pd.DataFrame, column: str | int, error_rate: float, error_mechanism: ErrorMechanism, error_type: ErrorType -) -> tuple[pd.DataFrame, pd.DataFrame]: - """Creates errors in a given column of a pandas DataFrame. + data: IntoDataFrame, column: str | int, error_rate: float, error_mechanism: ErrorMechanism, error_type: ErrorType +) -> tuple[IntoDataFrame, IntoDataFrame]: + """Creates errors in a given column of a DataFrame. Args: - data (pd.DataFrame): The pandas DataFrame to create errors in. + data (IntoDataFrame): The DataFrame to create errors in. Supports pandas, Polars, and other narwhals-compatible backends. column (str | int): The column to create errors in. error_rate (float): The rate at which errors will be created. error_mechanism (ErrorMechanism): The mechanism, controls the error distribution. error_type (ErrorType): The type of the error that will be distributed. Returns: - tuple[pd.DataFrame, pd.DataFrame]: + tuple[IntoDataFrame, IntoDataFrame]: - The first element is a copy of 'data' with errors. - The second element is the associated error mask. + Both are returned in the same format as the input data. """ + # Wrap native DataFrame to narwhals + data_nw = nw.from_native(data, eager_only=True) + check_error_rate(error_rate) - check_data_emptiness(data) - data_copy = data.copy() + check_data_emptiness(data_nw) + + # Clone the data to avoid modifying the original + data_copy = data_nw.clone() error_mask = error_mechanism.sample(data_copy, column, error_rate, error_mask=None) series = error_type.apply(data_copy, error_mask, column) - set_column(data_copy, column, series) + data_copy = set_column(data_copy, column, series) - return data_copy, error_mask + # Return in the original format + return nw.to_native(data_copy), nw.to_native(error_mask) diff --git a/tab_err/api/mid_level.py b/tab_err/api/mid_level.py index 22eb6d0..5f8a125 100644 --- a/tab_err/api/mid_level.py +++ b/tab_err/api/mid_level.py @@ -3,11 +3,13 @@ import dataclasses from typing import TYPE_CHECKING, Any -import pandas as pd +import narwhals as nw -from tab_err._utils import check_data_emptiness, check_error_rate, set_column +from tab_err._utils import check_data_emptiness, check_error_rate, create_empty_boolean_mask, set_column if TYPE_CHECKING: + from narwhals.typing import IntoDataFrame + from tab_err._error_model import ErrorModel @@ -35,22 +37,26 @@ def from_dict(data: dict[str, Any]) -> MidLevelConfig: return MidLevelConfig(**data) -def create_errors(data: pd.DataFrame, config: MidLevelConfig | dict) -> tuple[pd.DataFrame, pd.DataFrame]: +def create_errors(data: IntoDataFrame, config: MidLevelConfig | dict) -> tuple[IntoDataFrame, IntoDataFrame]: """Creates errors in a given DataFrame, following a user-defined configuration. Args: - data (pd.DataFrame): The pandas DataFrame to create errors in. + data (IntoDataFrame): The DataFrame to create errors in. Supports pandas, Polars, and other narwhals-compatible backends. config (MidLevelConfig | dict): The configuration for the error generation process. Returns: - tuple[pd.DataFrame, pd.DataFrame]: + tuple[IntoDataFrame, IntoDataFrame]: - The first element is a copy of 'data' with errors. - The second element is the associated error mask. + Both are returned in the same format as the input data. Raises: TypeError: If `config` has incorrect type. """ - check_data_emptiness(data) + # Wrap native DataFrame to narwhals + data_nw = nw.from_native(data, eager_only=True) + + check_data_emptiness(data_nw) if isinstance(config, dict): _config = MidLevelConfig(config) @@ -61,8 +67,8 @@ def create_errors(data: pd.DataFrame, config: MidLevelConfig | dict) -> tuple[pd msg = f"The type of 'config' must be either MidLevelConfig or dict but was {type(config)}." raise TypeError(msg) - data_dirty = data.copy() - error_mask = pd.DataFrame(data=False, index=data.index, columns=data.columns) + data_dirty = data_nw.clone() + error_mask = create_empty_boolean_mask(data_nw) for column in _config.columns: for error_model in _config.columns[column]: @@ -72,10 +78,20 @@ def create_errors(data: pd.DataFrame, config: MidLevelConfig | dict) -> tuple[pd error_type = error_model.error_type error_rate = error_model.error_rate - old_error_mask = error_mask.copy() - error_mask = error_mechanism.sample(data, column, error_rate, error_mask) + old_error_mask = error_mask.clone() + error_mask = error_mechanism.sample(data_nw, column, error_rate, error_mask) + + # Compute new error positions (where mask changed from False to True) + old_mask_col = old_error_mask[column if isinstance(column, str) else data_nw.columns[column]] + new_mask_col = error_mask[column if isinstance(column, str) else data_nw.columns[column]] + + # Create a mask for just the new errors + new_errors_arr = new_mask_col.to_numpy() & ~old_mask_col.to_numpy() + col_name = column if isinstance(column, str) else data_nw.columns[column] + new_errors_mask = old_error_mask.with_columns(nw.new_series(col_name, new_errors_arr.tolist(), backend=nw.get_native_namespace(old_error_mask))) - series = error_type.apply(data_dirty, old_error_mask != error_mask, column) - set_column(data_dirty, column, series) + series = error_type.apply(data_dirty, new_errors_mask, column) + data_dirty = set_column(data_dirty, column, series) - return data_dirty, error_mask + # Return in the original format + return nw.to_native(data_dirty), nw.to_native(error_mask) diff --git a/tab_err/error_mechanism/_ear.py b/tab_err/error_mechanism/_ear.py index b410f33..5788c6b 100644 --- a/tab_err/error_mechanism/_ear.py +++ b/tab_err/error_mechanism/_ear.py @@ -1,24 +1,23 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING + +import narwhals as nw +import numpy as np from tab_err._utils import check_error_rate, get_column, get_column_str from ._error_mechanism import ErrorMechanism -if TYPE_CHECKING: - import pandas as pd - class EAR(ErrorMechanism): - """`ErrorMechanism` subclass implementing the `Erroneous Completely At Random` error mechanism. + """`ErrorMechanism` subclass implementing the `Erroneous At Random` error mechanism. Description: - Errors are assumed to be completely independent of the data distribution + Errors are assumed to depend on another column's values """ - def _sample(self: EAR, data: pd.DataFrame, column: str | int, error_rate: float, error_mask: pd.DataFrame) -> pd.DataFrame: + def _sample(self: EAR, data: nw.DataFrame, column: str | int, error_rate: float, error_mask: nw.DataFrame) -> nw.DataFrame: """Creates an error mask according to the `Erroneous At Random` error mechanism. Description: @@ -28,17 +27,17 @@ def _sample(self: EAR, data: pd.DataFrame, column: str | int, error_rate: float, This ensures that occurrence of errors is related to the value of the another `column`. Args: - data (pd.DataFrame): `DataFrame` containins the column to add errors to + data (nw.DataFrame): `DataFrame` containing the column to add errors to column (str | int): The column of `data` to create an error mask for - error_rate (float): Proportion of rows to be affected by errors; in ranse [0,1] - error_mask (pd.DataFrame): A Pandas `DataFrame` with the same index & columns as `data` that will be modified and returned + error_rate (float): Proportion of rows to be affected by errors; in range [0,1] + error_mask (nw.DataFrame): A `DataFrame` with the same index & columns as `data` that will be modified and returned Raises: ValueError: If there are fewer than two columns in `data`, a `ValueError` will be returned ValueError: If there are insufficient entries to add errors to with respect to the error rate, a `ValueError` will be returned Returns: - pd.DataFrame: A Pandas `DataFrame` with `True` values at entries where an error should be introduced, `False` otherwise + nw.DataFrame: A `DataFrame` with `True` values at entries where an error should be introduced, `False` otherwise """ check_error_rate(error_rate) @@ -46,8 +45,9 @@ def _sample(self: EAR, data: pd.DataFrame, column: str | int, error_rate: float, msg = "The data into which error at random (EAR) are to be injected requires at least 2 columns." raise ValueError(msg) + col = get_column_str(data, column) + if self.condition_to_column is None: - col = get_column_str(data, column) column_selection = [x for x in data.columns if x != col] condition_to_column = self._random_generator.choice(column_selection) warnings.warn( @@ -60,22 +60,35 @@ def _sample(self: EAR, data: pd.DataFrame, column: str | int, error_rate: float, se_data = get_column(data, column) se_mask = get_column(error_mask, column) - n_errors = int(se_data.size * error_rate) + se_condition = get_column(data, condition_to_column) - se_mask_error_free = se_mask[~se_mask] - data_column_error_free = data.loc[se_mask_error_free.index, :] + n_errors = int(len(se_data) * error_rate) - if len(se_mask_error_free) < n_errors: + # Get arrays + mask_arr = se_mask.to_numpy() + condition_arr = se_condition.to_numpy() + + # Get indices where mask is False (error-free cells) + error_free_indices = np.where(~mask_arr)[0] + + if len(error_free_indices) < n_errors: msg = f"The error rate of {error_rate} requires {n_errors} error-free cells. " - msg += f"However, only {len(se_mask_error_free)} error-free cells are available." + msg += f"However, only {len(error_free_indices)} error-free cells are available." raise ValueError(msg) - # we offset the upper bound of the lower_error_index by a) the existing number of errors in the row, and b) the number of errors to-be generated. - upper_bound = len(se_data) - sum(se_mask) - n_errors # upper bound = length of data - current number of errors - number of errors to be generated + # Get values at error-free indices from the conditioning column and sort them + error_free_condition_values = condition_arr[error_free_indices] + sorted_order = np.argsort(error_free_condition_values) + sorted_error_free_indices = error_free_indices[sorted_order] + + # Calculate upper bound for lower_error_index + upper_bound = len(error_free_indices) - n_errors lower_error_index = self._random_generator.integers(0, upper_bound) if upper_bound > 0 else 0 - error_index_range = range(lower_error_index, lower_error_index + n_errors) - selected_rows = data_column_error_free.sort_values(by=condition_to_column).iloc[error_index_range, :] # Sort by the condition_to_column values + selected_indices = sorted_error_free_indices[lower_error_index : lower_error_index + n_errors] - se_mask.loc[selected_rows.index] = True + # Create new mask array with selected indices set to True + new_mask_arr = mask_arr.copy() + new_mask_arr[selected_indices] = True - return error_mask + # Update the error_mask DataFrame + return error_mask.with_columns(nw.new_series(col, new_mask_arr.tolist(), backend=nw.get_native_namespace(error_mask))) diff --git a/tab_err/error_mechanism/_ecar.py b/tab_err/error_mechanism/_ecar.py index 01d3aca..34d22fb 100644 --- a/tab_err/error_mechanism/_ecar.py +++ b/tab_err/error_mechanism/_ecar.py @@ -1,14 +1,13 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING -from tab_err._utils import check_error_rate, get_column +import narwhals as nw +import numpy as np -from ._error_mechanism import ErrorMechanism +from tab_err._utils import check_error_rate, get_column, get_column_str -if TYPE_CHECKING: - import pandas as pd +from ._error_mechanism import ErrorMechanism class ECAR(ErrorMechanism): @@ -20,43 +19,52 @@ class ECAR(ErrorMechanism): def _sample( self: ECAR, - data: pd.DataFrame, # noqa: ARG002 + data: nw.DataFrame, # noqa: ARG002 column: str | int, error_rate: float, - error_mask: pd.DataFrame, - ) -> pd.DataFrame: + error_mask: nw.DataFrame, + ) -> nw.DataFrame: """Creates an error mask according to the 'Erroneous Completely At Random' error mechanism. Description: - Sells are chosen uniform randomly by a NumPy random number generator + Cells are chosen uniform randomly by a NumPy random number generator Args: - data (pd.DataFrame): DataFrame containing the column to add errors to + data (nw.DataFrame): DataFrame containing the column to add errors to column (str | int): The column of 'data' to create an error mask for error_rate (float): Proportion of rows to be affected by errors; in range [0,1] - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned Raises: ValueError: If there are insufficient entries to add errors to with respect to the error rate, a ValueError will be returned Returns: - pd.DataFrame: A Pandas DataFrame with True values at entries where an error should be introduced, False otherwise + nw.DataFrame: A DataFrame with True values at entries where an error should be introduced, False otherwise """ check_error_rate(error_rate) + col_name = get_column_str(error_mask, column) se_mask = get_column(error_mask, column) - se_mask_error_free = se_mask[~se_mask] + + # Get indices where mask is False (error-free cells) + mask_arr = se_mask.to_numpy() + error_free_indices = np.where(~mask_arr)[0] if self.condition_to_column is not None: warnings.warn("'condition_to_column' is set but will be ignored by ECAR.", stacklevel=1) - n_errors = int(se_mask.size * error_rate) + n_errors = int(len(mask_arr) * error_rate) - if len(se_mask_error_free) < n_errors: + if len(error_free_indices) < n_errors: msg = f"The error rate of {error_rate} requires {n_errors} error-free cells. " - msg += f"However, only {len(se_mask_error_free)} error-free cells are available." + msg += f"However, only {len(error_free_indices)} error-free cells are available." raise ValueError(msg) # Uniform randomly choose error-cells - error_indices = self._random_generator.choice(se_mask_error_free.index, n_errors, replace=False) - se_mask[error_indices] = True - return error_mask + error_indices = self._random_generator.choice(error_free_indices, n_errors, replace=False) + + # Create new mask array with selected indices set to True + new_mask_arr = mask_arr.copy() + new_mask_arr[error_indices] = True + + # Update the error_mask DataFrame + return error_mask.with_columns(nw.new_series(col_name, new_mask_arr.tolist(), backend=nw.get_native_namespace(error_mask))) diff --git a/tab_err/error_mechanism/_enar.py b/tab_err/error_mechanism/_enar.py index c7d63f6..7f7b00a 100644 --- a/tab_err/error_mechanism/_enar.py +++ b/tab_err/error_mechanism/_enar.py @@ -1,14 +1,13 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING -from tab_err._utils import check_error_rate, get_column +import narwhals as nw +import numpy as np -from ._error_mechanism import ErrorMechanism +from tab_err._utils import check_error_rate, get_column, get_column_str -if TYPE_CHECKING: - import pandas as pd +from ._error_mechanism import ErrorMechanism class ENAR(ErrorMechanism): @@ -18,7 +17,7 @@ class ENAR(ErrorMechanism): Errors are assumed to depend on either other variables, the incorrect data itself, or both. """ - def _sample(self: ENAR, data: pd.DataFrame, column: str | int, error_rate: float, error_mask: pd.DataFrame) -> pd.DataFrame: + def _sample(self: ENAR, data: nw.DataFrame, column: str | int, error_rate: float, error_mask: nw.DataFrame) -> nw.DataFrame: """Creates an error mask according to the `Erroneous Not At Random` error mechanism. Description: @@ -29,40 +28,52 @@ def _sample(self: ENAR, data: pd.DataFrame, column: str | int, error_rate: float Args: - data (pd.DataFrame): DataFrame containing the column to add errors to + data (nw.DataFrame): DataFrame containing the column to add errors to column (str | int): The column of `data` to create an error mask for error_rate (float): Proportion of rows to be affected by errors; in range [0,1] - error_mask (pd.DataFrame): A Pandas `DataFrame` with the same index & columns as `data` that will be modified and returned + error_mask (nw.DataFrame): A `DataFrame` with the same index & columns as `data` that will be modified and returned Raises: ValueError: If there are insufficient entries to add errors to with respect to the error rate, `a` ValueError will be returned Returns: - pd.DataFrame: A Pandas DataFrame with `True` values at entries where an error should be introduced, `False` otherwise + nw.DataFrame: A DataFrame with `True` values at entries where an error should be introduced, `False` otherwise """ check_error_rate(error_rate) + col_name = get_column_str(data, column) se_data = get_column(data, column) se_mask = get_column(error_mask, column) if self.condition_to_column is not None: warnings.warn("'condition_to_column' is set but will be ignored by ENAR.", stacklevel=1) - n_errors = int(len(se_data) * error_rate) + n_rows = len(se_data) + n_errors = int(n_rows * error_rate) - # if mid-level or high-level API call ENAR, the error_mask already contains errors. Below we make sure that we only sample rows that do not - # already contain errors. - se_data_error_free = se_data[~se_mask] + # Get arrays + data_arr = se_data.to_numpy() + mask_arr = se_mask.to_numpy() - if len(se_data_error_free) < n_errors: + # Get indices where mask is False (error-free cells) + error_free_indices = np.where(~mask_arr)[0] + + if len(error_free_indices) < n_errors: msg = f"The error rate of {error_rate} requires {n_errors} error-free cells. " - msg += f"However, only {len(se_data_error_free)} error-free cells are available." + msg += f"However, only {len(error_free_indices)} error-free cells are available." raise ValueError(msg) - # TODO(anyone): ensure that the implementation is consistent between the ear and enar implementations of _sample -- upper_bound_variable? - lower_error_index = self._random_generator.integers(0, len(se_data_error_free) - n_errors) if len(se_data_error_free) != n_errors else 0 - error_index_range = range(lower_error_index, lower_error_index + n_errors) - selected_rows = se_data_error_free.sort_values().iloc[error_index_range] # Introduce errors to locations of sorted values + # Get values at error-free indices and sort them + error_free_values = data_arr[error_free_indices] + sorted_order = np.argsort(error_free_values) + sorted_error_free_indices = error_free_indices[sorted_order] + + # Choose a contiguous block of indices from the sorted error-free data + lower_error_index = self._random_generator.integers(0, len(error_free_indices) - n_errors) if len(error_free_indices) != n_errors else 0 + selected_indices = sorted_error_free_indices[lower_error_index : lower_error_index + n_errors] - se_mask.loc[selected_rows.index] = True + # Create new mask array with selected indices set to True + new_mask_arr = mask_arr.copy() + new_mask_arr[selected_indices] = True - return error_mask + # Update the error_mask DataFrame + return error_mask.with_columns(nw.new_series(col_name, new_mask_arr.tolist(), backend=nw.get_native_namespace(error_mask))) diff --git a/tab_err/error_mechanism/_error_mechanism.py b/tab_err/error_mechanism/_error_mechanism.py index d85934f..7427676 100644 --- a/tab_err/error_mechanism/_error_mechanism.py +++ b/tab_err/error_mechanism/_error_mechanism.py @@ -3,9 +3,9 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING -import pandas as pd +import narwhals as nw -from tab_err._utils import seed_randomness_and_get_generator +from tab_err._utils import create_empty_boolean_mask, seed_randomness_and_get_generator if TYPE_CHECKING: import numpy as np @@ -40,12 +40,12 @@ def __init__(self: ErrorMechanism, condition_to_column: int | str | None = None, def sample( self: ErrorMechanism, - data: pd.DataFrame, + data: nw.DataFrame, column: str | int, error_rate: float, - error_mask: pd.DataFrame | None = None, - ) -> pd.DataFrame: - """Returns an error mask for locations to introduce errors in a pandas DataFrame. + error_mask: nw.DataFrame | None = None, + ) -> nw.DataFrame: + """Returns an error mask for locations to introduce errors in a DataFrame. Description: Does error checking for the abstract method '_sample'. @@ -53,24 +53,24 @@ def sample( Calls subclass _sample method. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to + data (nw.DataFrame): DataFrame containing the column to add errors to column (str | int): The column of 'data' to create an error mask for error_rate (float): Percentage of rows to be affected by errors in range [0,1]. - error_mask (pd.DataFrame | None, optional): An existing error mask to add more errors to in the case of the mid-/high-level APIs. Defaults to None. + error_mask (nw.DataFrame | None, optional): An existing error mask to add more errors to in the case of the mid-/high-level APIs. Defaults to None. Raises: ValueError: If error rate is out of the [0,1] interval, a ValueError is thrown - TypeError: If the 'data' argument is not a pandas dataframe or the data is empty, a TypeError is thrown + TypeError: If the 'data' argument is not a DataFrame or the data is empty, a TypeError is thrown ValueError: If required and there are not 2 columns in the 'data' argument, a ValueError is thrown. Returns: - pd.DataFrame: Updated dataframe with the generated error mask + nw.DataFrame: Updated dataframe with the generated error mask """ if error_rate < 0 or error_rate > 1: error_rate_msg = "'error_rate' need to be float: 0 <= error_rate <= 1." raise ValueError(error_rate_msg) - if not isinstance(data, pd.DataFrame) or data.empty: + if not isinstance(data, nw.DataFrame) or data.is_empty(): data_msg = "'data' needs to be a non-empty DataFrame." raise TypeError(data_msg) @@ -84,21 +84,21 @@ def sample( # already inserted errors into, we have error mechanisms sample only from cells that # do not contain errors. if error_mask is None: # initialize empty error_mask - error_mask = pd.DataFrame(data=False, index=data.index, columns=data.columns) + error_mask = create_empty_boolean_mask(data) self._random_generator = seed_randomness_and_get_generator(self._seed) return self._sample(data, column, error_rate, error_mask) @abstractmethod - def _sample(self: ErrorMechanism, data: pd.DataFrame, column: str | int, error_rate: float, error_mask: pd.DataFrame) -> pd.DataFrame: - """Abstract method for the creation of an error mask over a given Pandas DataFrame. + def _sample(self: ErrorMechanism, data: nw.DataFrame, column: str | int, error_rate: float, error_mask: nw.DataFrame) -> nw.DataFrame: + """Abstract method for the creation of an error mask over a given DataFrame. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to + data (nw.DataFrame): DataFrame containing the column to add errors to column (str | int): The column of `data` to create an error mask for error_rate (float): Proportion of rows to be affected by errors; in range [0,1] - error_mask (pd.DataFrame): A Pandas `DataFrame` with the same index & columns as `data` that will be modified and returned + error_mask (nw.DataFrame): A `DataFrame` with the same index & columns as `data` that will be modified and returned Returns: - pd.DataFrame: A Pandas `DataFrame` with `True` values at entries where an error should be introduced, `False` otherwise + nw.DataFrame: A `DataFrame` with `True` values at entries where an error should be introduced, `False` otherwise """ diff --git a/tab_err/error_type/_add_delta.py b/tab_err/error_type/_add_delta.py index e19c73a..b065a55 100644 --- a/tab_err/error_type/_add_delta.py +++ b/tab_err/error_type/_add_delta.py @@ -1,11 +1,13 @@ from __future__ import annotations import warnings +from typing import TYPE_CHECKING -import pandas as pd -from pandas.api.types import is_datetime64_dtype, is_numeric_dtype +if TYPE_CHECKING: + import narwhals as nw +import numpy as np -from tab_err._utils import get_column +from tab_err._utils import get_column, is_datetime_dtype, is_numeric_dtype, new_series_like, select_numeric_or_datetime_columns from ._error_type import ErrorType @@ -14,49 +16,61 @@ class AddDelta(ErrorType): """Adds a delta to values in a column.""" @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: series = get_column(data, column) - if not (is_numeric_dtype(series) or is_datetime64_dtype(series)): + if not (is_numeric_dtype(series) or is_datetime_dtype(series)): msg = f"Column {column} with dtype: {series.dtype} does not contain numeric or datetime64 values. Cannot apply AddDelta." raise TypeError(msg) - def _get_valid_columns(self: AddDelta, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: AddDelta, data: nw.DataFrame) -> list[str | int]: """Returns all column names with numeric dtype elements.""" - return data.select_dtypes(include=["number", "datetime64"]).columns.tolist() + return select_numeric_or_datetime_columns(data) - def _apply(self: AddDelta, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: AddDelta, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the AddDelta ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Raises: ValueError: If the add_delta_value is None, a ValueError will be thrown. Returns: - pd.Series: The data column, 'column', after AddDelta errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after AddDelta errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) - was_datetime = False # Default was_datetime to false -- changes occur only in the special case of datetime + was_datetime = False + + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() - if is_datetime64_dtype(series): # Convert to int (number of seconds) if datetime - series = series.astype("int64") // 10**9 + if is_datetime_dtype(series): + # Convert datetime to seconds since epoch + data_arr = data_arr.astype("datetime64[ns]").astype("int64") // 10**9 was_datetime = True + # Ensure float for calculations + data_arr = data_arr.astype(np.float64) + if self.config.add_delta_value is None: msg = f"self.config.add_delta_value is none, sampling a random delta value uniformly from the range of column: {column}." warnings.warn(msg, stacklevel=2) - self.config.add_delta_value = ( - self._random_generator.choice(series) - series.mean() - ) / series.std() # Ensures a smaller value than uniform sampling + mean_val = np.nanmean(data_arr) + std_val = np.nanstd(data_arr) + random_choice = self._random_generator.choice(data_arr[~np.isnan(data_arr)]) + delta_value = (random_choice - mean_val) / std_val if std_val != 0 else 0 + else: + delta_value = self.config.add_delta_value - series = series.where(~series_mask, series + self.config.add_delta_value) # Avoids in-place modification + data_arr[mask_arr] += delta_value - if was_datetime: # Convert back to datetime if it was initially - series = pd.to_datetime(series, unit="s") + if was_datetime: + # Convert back to datetime (from seconds) + data_arr = (data_arr * 10**9).astype("int64").astype("datetime64[ns]") - return series + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_category_swap.py b/tab_err/error_type/_category_swap.py index 429f956..7c8e9f3 100644 --- a/tab_err/error_type/_category_swap.py +++ b/tab_err/error_type/_category_swap.py @@ -2,9 +2,9 @@ import random -import pandas as pd +import narwhals as nw -from tab_err._utils import get_column +from tab_err._utils import get_column, new_series_like from ._error_type import ErrorType @@ -13,11 +13,11 @@ class CategorySwap(ErrorType): """Simulate incorrect labels in a column that contains categorical values.""" @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: """Checks that the data type is Categorical and the number of categories is at least two in the column to be modified. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. + data (nw.DataFrame): DataFrame containing the column to add errors to. column (int | str): The column of 'data' to create an error mask for. Raises: @@ -26,57 +26,81 @@ def _check_type(data: pd.DataFrame, column: int | str) -> None: """ series = get_column(data, column) - if not isinstance(series.dtype, pd.CategoricalDtype): - msg = f"Column {column} does not contain values of the Categorical dtype. Cannot insert Mislables.\n" - msg += "Try casting the column to CategoricalDtype using df[column].astype('category')." + if series.dtype != nw.Categorical: + msg = f"Column {column} does not contain values of the Categorical dtype. Cannot insert Mislabels.\n" + msg += "Try casting the column to Categorical dtype." raise TypeError(msg) - if len(series.cat.categories) <= 1: - msg = f"Column {column} contains {len(series.cat.categories)} categories. Require at least 2 categories to insert mislabels." + # Get unique values to check number of categories + unique_vals = series.unique() + if len(unique_vals) <= 1: + msg = f"Column {column} contains {len(unique_vals)} categories. Require at least 2 categories to insert mislabels." raise ValueError(msg) - def _get_valid_columns(self: CategorySwap, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: CategorySwap, data: nw.DataFrame) -> list[str | int]: """Checks which columns are categorical and returns the indices of those with two or more categories.""" - valid_columns = [] + valid_columns: list[str | int] = [] for col_name in data.columns: series = get_column(data, col_name) - if isinstance(series.dtype, pd.CategoricalDtype) and len(series.cat.categories) > 1: - valid_columns.append(col_name) + if series.dtype == nw.Categorical: + unique_vals = series.unique() + if len(unique_vals) > 1: + valid_columns.append(col_name) return valid_columns - def _apply(self: CategorySwap, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: CategorySwap, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the CategorySwap ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Raises: ValueError: If the value for parameter 'config.mislabel_weighing' is invalid (not 'uniform' or 'frequency'), a ValueError will be thrown. Returns: - pd.Series: The data column, 'column', after CategorySwap errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after CategorySwap errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + series = get_column(data, column) + series_mask = get_column(error_mask, column) + + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() + + # Get categories + categories = series.unique().to_numpy() if self.config.mislabel_weighing == "uniform": - def sample_label(old_label: pd.Series) -> pd.Series: - choices = [x for x in series.cat.categories.to_numpy() if x != old_label] + def sample_label(old_label: str) -> str: + choices = [x for x in categories if x != old_label] return random.choice(choices) elif self.config.mislabel_weighing == "frequency": - - def sample_label(old_label: pd.Series) -> pd.Series: - se_sample = series.loc[series != old_label] - return se_sample.sample(1, replace=True).to_numpy()[0] + # Calculate frequency weights + value_counts: dict[str, int] = {} + for val in data_arr: + if val not in value_counts: + value_counts[val] = 0 + value_counts[val] += 1 + + def sample_label(old_label: str) -> str: + choices = [x for x in categories if x != old_label] + weights: list[float] = [float(value_counts.get(x, 1)) for x in choices] + total = float(sum(weights)) + weights = [w / total for w in weights] + return random.choices(choices, weights=weights, k=1)[0] else: msg = "Invalid value for parameter 'config.mislabel_weighing'. Allowed values are: 'uniform', 'frequency'." raise ValueError(msg) - series_mask = get_column(error_mask, column) - series.loc[series_mask] = series.loc[series_mask].apply(sample_label) - return series + # Apply mislabeling where mask is True + for i in range(len(data_arr)): + if mask_arr[i]: + data_arr[i] = sample_label(data_arr[i]) + + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_error_type.py b/tab_err/error_type/_error_type.py index 6be42f4..ce81d0e 100644 --- a/tab_err/error_type/_error_type.py +++ b/tab_err/error_type/_error_type.py @@ -8,8 +8,8 @@ from ._config import ErrorTypeConfig if TYPE_CHECKING: + import narwhals as nw import numpy as np - import pandas as pd class ErrorType(ABC): @@ -46,16 +46,16 @@ def __init__(self: ErrorType, config: ErrorTypeConfig | dict | None = None, seed self._seed = seed self._random_generator: np.random.Generator - def apply(self: ErrorType, data: pd.DataFrame, error_mask: pd.DataFrame, column: str | int) -> pd.Series: + def apply(self: ErrorType, data: nw.DataFrame, error_mask: nw.DataFrame, column: str | int) -> nw.Series: """Applies an ErrorType to a column of 'data'. Does type and shape checking and creates a random number generator. Args: - data (pd.DataFrame): The Pandas DataFrame containing the column where errors are to be introduced. - error_mask (pd.DataFrame): The Pandas DataFrame containing the error mask for 'column'. + data (nw.DataFrame): The DataFrame containing the column where errors are to be introduced. + error_mask (nw.DataFrame): The DataFrame containing the error mask for 'column'. column (str | int): The index in the 'data' and 'error_mask' DataFrames where errors are to be introduced. Returns: - pd.Series: The data column, 'column', after errors of ErrorType at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after errors of ErrorType at the locations specified by 'error_mask' are introduced. """ self._check_type(data, column) @@ -66,35 +66,35 @@ def apply(self: ErrorType, data: pd.DataFrame, error_mask: pd.DataFrame, column: self._random_generator = seed_randomness_and_get_generator(self._seed) return self._apply(data, error_mask, column) - def get_valid_columns(self: ErrorType, data: pd.DataFrame) -> list[str | int]: + def get_valid_columns(self: ErrorType, data: nw.DataFrame) -> list[str | int]: """Finds the valid columns to which the error type can be applied. Wrapper around _get_valid_columns.""" return self._get_valid_columns(data) @staticmethod @abstractmethod - def _check_type(data: pd.DataFrame, column: str | int) -> None: + def _check_type(data: nw.DataFrame, column: str | int) -> None: """Static abstract method that checks if the given columns are valid for this 'ErrorType'. Args: - data (pd.DataFrame): The Pandas DataFrame containing the column where errors are to be introduced. + data (nw.DataFrame): The DataFrame containing the column where errors are to be introduced. column (str | int): The 'column' of 'data' where errors are to be introduced. """ @abstractmethod - def _get_valid_columns(self: ErrorType, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: ErrorType, data: nw.DataFrame) -> list[str | int]: """Finds the valid columns to which the error type can be applied.""" @abstractmethod - def _apply(self: ErrorType, data: pd.DataFrame, error_mask: pd.DataFrame, column: str | int) -> pd.Series: + def _apply(self: ErrorType, data: nw.DataFrame, error_mask: nw.DataFrame, column: str | int) -> nw.Series: """Abstract method for the application of an ErrorType to the cells in 'data' where 'error_mask' is True. Args: - data (pd.DataFrame): The Pandas DataFrame containing the column where errors are to be introduced. - error_mask (pd.DataFrame): The Pandas DataFrame containing the error mask for 'column'. + data (nw.DataFrame): The DataFrame containing the column where errors are to be introduced. + error_mask (nw.DataFrame): The DataFrame containing the error mask for 'column'. column (str | int): The index in the 'data' and 'error_mask' DataFrames where errors are to be introduced. Returns: - pd.Series: The data column, 'column', after errors of ErrorType at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after errors of ErrorType at the locations specified by 'error_mask' are introduced. """ def to_dict(self: ErrorType) -> dict[str, Any]: diff --git a/tab_err/error_type/_extraneous.py b/tab_err/error_type/_extraneous.py index a542209..af24179 100644 --- a/tab_err/error_type/_extraneous.py +++ b/tab_err/error_type/_extraneous.py @@ -4,12 +4,12 @@ import warnings from typing import TYPE_CHECKING -from tab_err._utils import get_column +from tab_err._utils import get_column, new_series_like, select_string_columns from ._error_type import ErrorType if TYPE_CHECKING: - import pandas as pd + import narwhals as nw class Extraneous(ErrorType): @@ -25,31 +25,35 @@ def _generate_value_template_string(self: Extraneous, min_n: int = 0, max_n: int return prepend + r"{value}" + append @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: # all data types are fine pass - def _get_valid_columns(self: Extraneous, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: Extraneous, data: nw.DataFrame) -> list[str | int]: """Returns all column names with string dtype elements. Necessary for high level API.""" - return data.select_dtypes(include=["string", "object"]).columns.to_list() + return select_string_columns(data) - def _apply(self: Extraneous, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: Extraneous, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the Extraneous ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Raises: ValueError: If extraneous_value_template does not contain the placeholder value, a ValueError will be thrown. Returns: - pd.Series: The data column, 'column', after Extraneous errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Extraneous errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() + if self.config.extraneous_value_template is None: msg = "self.config.extraneous_value_template is not set. Choosing a random string augmentation." warnings.warn(msg, stacklevel=2) @@ -60,5 +64,10 @@ def _apply(self: Extraneous, data: pd.DataFrame, error_mask: pd.DataFrame, colum msg += "{value}. Please add it for a valid format." raise ValueError(msg) - series.loc[series_mask] = series.loc[series_mask].apply(lambda x: self.config.extraneous_value_template.format(value=x)) - return series + # Apply extraneous template where mask is True + for i in range(len(data_arr)): + if mask_arr[i]: + val = data_arr[i] + data_arr[i] = self.config.extraneous_value_template.format(value=val) + + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_missing.py b/tab_err/error_type/_missing.py index 87373aa..ae1add3 100644 --- a/tab_err/error_type/_missing.py +++ b/tab_err/error_type/_missing.py @@ -1,49 +1,63 @@ from __future__ import annotations -import pandas as pd -from pandas.api.types import is_string_dtype +from typing import TYPE_CHECKING, cast -from tab_err._utils import get_column +from tab_err._utils import get_column, is_string_dtype, new_series_like, select_string_columns from ._error_type import ErrorType +if TYPE_CHECKING: + import narwhals as nw + class MissingValue(ErrorType): """Insert missing values into a column. - Missing value handling is not a solved problem in pandas and under active development. - Today, the best heuristic for inserting missing values is to assign None to the value. - Pandas will choose the missing value sentinel based on the column dtype - (https://pandas.pydata.org/docs/user_guide/missing_data.html#inserting-missing-data). + Missing value handling varies across DataFrame libraries. This implementation + inserts None/null values which will be handled appropriately by the underlying library. """ @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: # all dtypes are supported pass - def _get_valid_columns(self: MissingValue, data: pd.DataFrame) -> list[str | int]: - """If the config mising value is None, returns all columns. Otherwise, only the columns with the same type.""" - return data.columns.to_list() if self.config.missing_value is None else data.select_dtypes(include=["object", "string"]).columns.to_list() + def _get_valid_columns(self: MissingValue, data: nw.DataFrame) -> list[str | int]: + """If the config missing value is None, returns all columns. Otherwise, only the columns with string type.""" + if self.config.missing_value is None: + return cast("list[str | int]", list(data.columns)) + return select_string_columns(data) - def _apply(self: MissingValue, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: MissingValue, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the MissingValue ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Returns: - pd.Series: The data column, 'column', after MissingValue errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after MissingValue errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) - if is_string_dtype(series) and self.config.missing_value is None: # Strings are finicky - series[series_mask] = pd.NA - series = series.astype(str) - else: - series[series_mask] = self.config.missing_value + # Get numpy arrays for manipulation + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() - return series + # Set values to missing_value or None where mask is True + if is_string_dtype(series) and self.config.missing_value is None: + # For string columns, convert to object array to allow None + data_arr = data_arr.astype(object) + data_arr[mask_arr] = None + else: + # For other types, use the configured missing value or None + missing_val = self.config.missing_value + if missing_val is None: + # Convert to object to allow None + data_arr = data_arr.astype(object) + data_arr[mask_arr] = missing_val + + # Create new series with the modified data + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_mistype.py b/tab_err/error_type/_mistype.py index bb07136..6d8c104 100644 --- a/tab_err/error_type/_mistype.py +++ b/tab_err/error_type/_mistype.py @@ -2,36 +2,57 @@ from typing import TYPE_CHECKING -from tab_err._utils import get_column - -from ._error_type import ErrorType +import narwhals as nw if TYPE_CHECKING: - import pandas as pd + import numpy as np + +from tab_err._utils import get_column, get_column_str + +from ._error_type import ErrorType class Mistype(ErrorType): """Insert incorrectly typed values into a column. Note that the dtype of the column is changed by this operation. - String / Object is the dead end of typing - In an effort to keep the code relatively simple, we cast the corrupted column to an Object Dtype. + In an effort to keep the code relatively simple, we cast the corrupted column to an Object dtype. """ @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: # all dtypes are supported pass - def _get_valid_columns(self: Mistype, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: Mistype, data: nw.DataFrame) -> list[str | int]: """Returns all column names of columns with dtypes other than object. This is necessary for the high level API.""" - return [col_name for col_name in data.columns.tolist() if data[col_name].dtype != "object"] - - def _apply(self: Mistype, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + return [col_name for col_name in data.columns if data[col_name].dtype != nw.Object] + + def _convert_dtypes(self, data_arr: np.ndarray, mask_arr: np.ndarray, target_dtype: str) -> np.ndarray: + # Convert to object array to allow mixed types + result_arr = data_arr.astype(object) + + # Apply type conversion where mask is True + for i in range(len(result_arr)): + if mask_arr[i]: + val = result_arr[i] + if target_dtype in ("int64", "Int64"): + result_arr[i] = int(val) if val is not None else val + elif target_dtype in ("float64", "Float64"): + result_arr[i] = float(val) if val is not None else val + elif target_dtype in ("object", "string"): + result_arr[i] = str(val) if val is not None else val + else: + msg = f"Unsupported dtype to cast into: {target_dtype}" + raise ValueError(msg) + return result_arr + + def _apply(self: Mistype, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the Mistype ErrorType to a column of data. Note that the dtype of the column is changed by this operation. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Raises: @@ -39,41 +60,45 @@ def _apply(self: Mistype, data: pd.DataFrame, error_mask: pd.DataFrame, column: TypeError: If no type is supplied by the user in the config, and the series' datatype is 'object', a TypeError will be thrown. Returns: - pd.Series: The data column, 'column', after Mistype errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Mistype errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + col_name = get_column_str(data, column) + series = get_column(data, column) + series_mask = get_column(error_mask, column) + supported_dtypes = ["object", "string", "int64", "Int64", "float64", "Float64"] if self.config.mistype_dtype is not None: if self.config.mistype_dtype not in supported_dtypes: - msg = f"Unsupported user-specified dtype {self.config.mistype_dtype}. Supported dtypes as {supported_dtypes}." + msg = f"Unsupported user-specified dtype {self.config.mistype_dtype}. Supported dtypes are {supported_dtypes}." raise TypeError(msg) target_dtype = self.config.mistype_dtype - else: # no user-specified dtype, use heuristict to infer one + # no user-specified dtype, use heuristic to infer one -- that's the hard-coded logic below, with pairs of + # current_dtype and target_dtype that can be cast without raising errors. Not very principled approach, but + # works as a fallback. + else: current_dtype = series.dtype - if current_dtype == "object": + if current_dtype == nw.Object: msg = "Cannot infer a dtype that is safe to cast to if the original dtype is 'object'." raise TypeError(msg) - if current_dtype == "string": + if current_dtype == nw.String: target_dtype = "object" - elif current_dtype == "int64": + elif current_dtype == nw.Int64: target_dtype = "float64" - elif current_dtype == "Int64": - target_dtype = "Float64" - elif current_dtype == "float64": + elif current_dtype in {nw.Float64, nw.Boolean}: target_dtype = "int64" - elif current_dtype == "Float64": - target_dtype = "Int64" - elif current_dtype == "bool": + elif current_dtype.is_integer(): + target_dtype = "float64" + elif current_dtype.is_numeric(): target_dtype = "int64" else: msg = f"The type: {current_dtype} is unsupported. The type must be one of: {*supported_dtypes, 'bool'}." raise ValueError(msg) - # NOTE(PJ): not sure about this logic, there might be a better way to do this. - series = series.astype("object") - series_mask = get_column(error_mask, column) - series.loc[series_mask] = series.loc[series_mask].astype(target_dtype) + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() - return series + result_arr = self._convert_dtypes(data_arr, mask_arr, target_dtype) + return nw.new_series(col_name, result_arr.tolist(), backend=nw.get_native_namespace(data)) diff --git a/tab_err/error_type/_mojibake.py b/tab_err/error_type/_mojibake.py index a7f12e6..407f126 100644 --- a/tab_err/error_type/_mojibake.py +++ b/tab_err/error_type/_mojibake.py @@ -3,41 +3,39 @@ import random from typing import TYPE_CHECKING -from pandas.api.types import is_string_dtype - -from tab_err._utils import get_column +from tab_err._utils import get_column, is_string_dtype, new_series_like, select_string_columns from ._error_type import ErrorType if TYPE_CHECKING: - import pandas as pd + import narwhals as nw class Mojibake(ErrorType): """Inserts mojibake into a column containing strings.""" @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: series = get_column(data, column) if not is_string_dtype(series): msg = f"Column {column} does not contain values of the string dtype. Cannot insert Mojibake." raise TypeError(msg) - def _get_valid_columns(self: Mojibake, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: Mojibake, data: nw.DataFrame) -> list[str | int]: """Returns all column names with string dtype elements.""" - return data.select_dtypes(include=["string", "object"]).columns.to_list() + return select_string_columns(data) - def _apply(self: Mojibake, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: Mojibake, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the Mojibake ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Returns: - pd.Series: The data column, 'column', after Mojibake errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Mojibake errors at the locations specified by 'error_mask' are introduced. """ # Top 10 most used encodings on the internet # https://w3techs.com/technologies/overview/character_encoding @@ -57,7 +55,9 @@ def _apply(self: Mojibake, data: pd.DataFrame, error_mask: pd.DataFrame, column: "iso-8859-2": top10 - {"iso-8859-2", "windows-1250", "iso-8859-1", "windows-1252"}, } - series = get_column(data, column).copy() + series = get_column(data, column) + series_mask = get_column(error_mask, column) + encoding_sender = self.config.encoding_sender encoding_receiver = self.config.encoding_receiver @@ -67,8 +67,15 @@ def _apply(self: Mojibake, data: pd.DataFrame, error_mask: pd.DataFrame, column: encoding_sender = random.choice(list(top10)) encoding_receiver = random.choice(list(encodings[encoding_sender])) - series_mask = get_column(error_mask, column) - series.loc[series_mask] = ( - series.loc[series_mask].apply(lambda x: x.encode(encoding_sender, errors="ignore")).apply(lambda x: x.decode(encoding_receiver, errors="ignore")) - ) - return series + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() + + # Apply mojibake where mask is True + for i in range(len(data_arr)): + if mask_arr[i]: + val = data_arr[i] + if val is not None and isinstance(val, str): + data_arr[i] = val.encode(encoding_sender, errors="ignore").decode(encoding_receiver, errors="ignore") + + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_outlier.py b/tab_err/error_type/_outlier.py index 4c2303b..cc2b4d3 100644 --- a/tab_err/error_type/_outlier.py +++ b/tab_err/error_type/_outlier.py @@ -1,13 +1,16 @@ from __future__ import annotations +from typing import TYPE_CHECKING + import numpy as np -import pandas as pd -from pandas.api.types import is_datetime64_dtype, is_integer_dtype, is_numeric_dtype -from tab_err._utils import get_column +from tab_err._utils import get_column, is_datetime_dtype, is_integer_dtype, is_numeric_dtype, new_series_like, select_numeric_or_datetime_columns from ._error_type import ErrorType +if TYPE_CHECKING: + import narwhals as nw + class Outlier(ErrorType): """Inserts outliers into a column by pushing data points outside the interquartile range (IQR) boundaries. @@ -27,40 +30,47 @@ class Outlier(ErrorType): """ @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: series = get_column(data, column) - if not (is_numeric_dtype(series) or is_datetime64_dtype(series)): + if not (is_numeric_dtype(series) or is_datetime_dtype(series)): msg = f"Column {column} with dtype: {series.dtype} does not contain numeric or datetime64 values. Cannot apply outliers." raise TypeError(msg) - def _get_valid_columns(self: Outlier, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: Outlier, data: nw.DataFrame) -> list[str | int]: """Returns all column names with numeric dtype elements.""" - return data.select_dtypes(include=["number", "datetime64"]).columns.tolist() + return select_numeric_or_datetime_columns(data) - def _apply(self: Outlier, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: Outlier, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the Outlier ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Returns: - pd.Series: The data column, 'column', after Outlier errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Outlier errors at the locations specified by 'error_mask' are introduced. """ - # Get the column series and mask - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) - was_datetime = False # Default to false -- changes to code only occur if the series is datetime + was_datetime = False - if is_datetime64_dtype(series): # Convert to int if datetime (ns since UNIX epoch) -- We need to add robustness against intmax/floatmax - series = series.astype("int64") + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() + + if is_datetime_dtype(series): + # Convert datetime to int64 (nanoseconds since epoch) + data_arr = data_arr.astype("datetime64[ns]").astype("int64") was_datetime = True - mean_value = series.mean() - q1 = series.quantile(0.25) - q3 = series.quantile(0.75) + # Ensure float for calculations + data_arr = data_arr.astype(np.float64) + + mean_value = np.nanmean(data_arr) + q1 = np.nanquantile(data_arr, 0.25) + q3 = np.nanquantile(data_arr, 0.75) iqr = q3 - q1 upper_boundary = q3 + 1.5 * iqr @@ -70,32 +80,40 @@ def _apply(self: Outlier, data: pd.DataFrame, error_mask: pd.DataFrame, column: perturbation_upper = self.config.outlier_coefficient * (upper_boundary - mean_value) perturbation_lower = self.config.outlier_coefficient * (mean_value - lower_boundary) - if is_integer_dtype(series): # round float to int when series is int + is_integer = is_integer_dtype(series) and not was_datetime + if is_integer: perturbation_upper = np.ceil(perturbation_upper) perturbation_lower = np.floor(perturbation_lower) # Get masks for the different outlier types depending on the mean - mask_lower = (series < mean_value) & series_mask - mask_upper = (series > mean_value) & series_mask - mask_equal = (series == mean_value) & series_mask + mask_lower = (data_arr < mean_value) & mask_arr + mask_upper = (data_arr > mean_value) & mask_arr + mask_equal = (data_arr == mean_value) & mask_arr # Apply the constant perturbation to the respective mask - series.loc[mask_lower] -= perturbation_lower - series.loc[mask_upper] += perturbation_upper + data_arr[mask_lower] -= perturbation_lower + data_arr[mask_upper] += perturbation_upper # Handle the mean values with a coin flip - coin_flips = self._random_generator.random(mask_equal.sum()) - series.loc[mask_equal] += np.where(coin_flips > self.config.outlier_coin_flip_threshold, perturbation_upper, -perturbation_lower) + n_equal: int = int(np.sum(mask_equal)) + if n_equal > 0: + coin_flips = self._random_generator.random(n_equal) + perturbations = np.where(coin_flips > self.config.outlier_coin_flip_threshold, perturbation_upper, -perturbation_lower) + data_arr[mask_equal] += perturbations # Apply Gaussian noise to simulate the increase in measurement error of the outliers noise_std = self.config.outlier_noise_coeff * iqr + n_errors: int = int(np.sum(mask_arr)) - if is_integer_dtype(series): # round float to int when series is int - series.loc[series_mask] += np.rint(self._random_generator.normal(loc=0, scale=noise_std, size=series_mask.sum())) + if is_integer: + data_arr[mask_arr] += np.rint(self._random_generator.normal(loc=0, scale=noise_std, size=n_errors)) else: - series.loc[series_mask] += self._random_generator.normal(loc=0, scale=noise_std, size=series_mask.sum()) + data_arr[mask_arr] += self._random_generator.normal(loc=0, scale=noise_std, size=n_errors) - if was_datetime: # Handle datetime objects - series = pd.to_datetime(series) + if was_datetime: + # Convert back to datetime + data_arr = data_arr.astype("int64").astype("datetime64[ns]") + elif is_integer: + data_arr = data_arr.astype(np.int64) - return series + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_permutate.py b/tab_err/error_type/_permutate.py index b5a2dc5..896728f 100644 --- a/tab_err/error_type/_permutate.py +++ b/tab_err/error_type/_permutate.py @@ -3,15 +3,14 @@ import random from typing import TYPE_CHECKING -from pandas.api.types import is_string_dtype +if TYPE_CHECKING: + import narwhals as nw + import numpy as np -from tab_err._utils import get_column +from tab_err._utils import get_column, is_string_dtype, new_series_like, select_string_columns from ._error_type import ErrorType -if TYPE_CHECKING: - import pandas as pd - def _generate_shuffle_pattern(format_len: int) -> list[int]: """Generates a list of integers that indicates the positions of each value in a formatted string.""" @@ -36,16 +35,16 @@ class Permutate(ErrorType): """Permutates the parts of a compound value in a column.""" @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: series = get_column(data, column) if not is_string_dtype(series): msg = f"Column {column} does not contain values of the string dtype. Cannot Permutate values." raise TypeError(msg) - def _get_valid_columns(self: Permutate, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: Permutate, data: nw.DataFrame) -> list[str | int]: """Returns column names with string dtype elements.""" - return data.select_dtypes(include=["string", "object"]).columns.to_list() + return select_string_columns(data) def _random_pattern_function(self: Permutate, old_string: str) -> str: """Generates a random permutation of `old_string` elements split on the `permutation_separator`.""" @@ -63,42 +62,67 @@ def _fixed_pattern_function(self: Permutate, old_string: str, new_pattern: list[ return self.config.permutation_separator.join(new_string_as_part_list) - def _apply(self: Permutate, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _pattern_permutation(self, permutation_pattern: list[int], data_arr: np.ndarray, mask_arr: np.ndarray) -> np.ndarray: + """Permutates values of a series data_arr as specified in permutation_pattern.""" + for i in range(len(data_arr)): + if mask_arr[i]: + val = data_arr[i] + if val is not None and isinstance(val, str): + data_arr[i] = self._fixed_pattern_function(val, permutation_pattern) + return data_arr + + def _random_permutation(self, data_arr: np.ndarray, mask_arr: np.ndarray) -> np.ndarray: + """Permutates values of a series data_arr using a new, random pattern for each value.""" + for i in range(len(data_arr)): + if mask_arr[i]: + val = data_arr[i] + if val is not None and isinstance(val, str): + data_arr[i] = self._random_pattern_function(val) + + return data_arr + + def _apply(self: Permutate, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the `Permutate` `ErrorType` to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Raises: - ValueError: If the column conatins values not supported by the seperator, a ValueError will be thrown. + ValueError: If the column contains values not supported by the separator, a ValueError will be thrown. ValueError: If a fixed_permutation_pattern is selected and all values are not formatted the same way, a ValueError will be thrown. Returns: - pd.Series: The data column, 'column', after Permutate errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Permutate errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) - separator_counts = [x.count(self.config.permutation_separator) for x in series.dropna()] + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() + + # Get separator counts for non-null values + separator_counts = [val.count(self.config.permutation_separator) for val in data_arr if val is not None and isinstance(val, str)] + for i, count in enumerate(separator_counts): if count == 0: - msg = f'Cannot permutate values, because column {column} contains value "{series[i]}" that is not separated by the separator ' + msg = f'Cannot permutate values, because column {column} contains value "{data_arr[i]}" that is not separated by the separator ' msg += f'"{self.config.permutation_separator}". To use another separator, define it in the ErrorTypeConfig.' raise ValueError(msg) if self.config.permutation_pattern is not None: # Permutation of each entry from pattern. _check_column_format_consistency(separator_counts, column) - new_pattern = self.config.permutation_pattern - series.loc[series_mask] = series.loc[series_mask].apply(self._fixed_pattern_function, args=(new_pattern,)) + data_arr = self._pattern_permutation(self.config.permutation_pattern, data_arr, mask_arr) elif self.config.permutation_automation_pattern == "fixed": # Fixed permutation -- random once, applied to all. _check_column_format_consistency(separator_counts, column) - new_pattern = _generate_shuffle_pattern(separator_counts[0]) - series.loc[series_mask] = series.loc[series_mask].apply(self._fixed_pattern_function, args=(new_pattern,)) + rnd_permutation_pattern = _generate_shuffle_pattern(separator_counts[0]) + + data_arr = self._pattern_permutation(rnd_permutation_pattern, data_arr, mask_arr) else: # Random permutation -- random for each entry. - series.loc[series_mask] = series.loc[series_mask].apply(self._random_pattern_function) + data_arr = self._random_permutation(data_arr, mask_arr) - return series + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_replace.py b/tab_err/error_type/_replace.py index d754b40..d788543 100644 --- a/tab_err/error_type/_replace.py +++ b/tab_err/error_type/_replace.py @@ -3,50 +3,66 @@ import warnings from typing import TYPE_CHECKING -from pandas.api.types import is_string_dtype +if TYPE_CHECKING: + import narwhals as nw -from tab_err._utils import get_column +from tab_err._utils import get_column, is_string_dtype, new_series_like, select_string_columns from ._error_type import ErrorType -if TYPE_CHECKING: - import pandas as pd - class Replace(ErrorType): """Replace a part of strings within a column.""" @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: series = get_column(data, column) if not is_string_dtype(series): - msg = f"Column {column} does not contain values of the string dtype. Cannot Permutate values." + msg = f"Column {column} does not contain values of the string dtype. Cannot replace values." raise TypeError(msg) - def _get_valid_columns(self: Replace, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: Replace, data: nw.DataFrame) -> list[str | int]: """Returns column names with string dtype elements.""" - return data.select_dtypes(include=["string", "object"]).columns.to_list() + return select_string_columns(data) - def _apply(self: Replace, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: Replace, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the Replace ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Returns: - pd.Series: The data column, 'column', after Replace errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Replace errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() + if self.config.replace_what is None: msg = "The 'replace_what' parameter is not configured, defaulting to a random character from the given series. Replacements are not guaranteed." warnings.warn(msg, stacklevel=2) - random_row = self._random_generator.choice(series.index) - self.config.replace_what = self._random_generator.choice(list(series[random_row])) + # Get valid values (non-None strings) + valid_values = [v for v in data_arr if v is not None and isinstance(v, str) and len(v) > 0] + if valid_values: + random_row = self._random_generator.choice(len(valid_values)) + random_val = valid_values[random_row] + replace_what = self._random_generator.choice(list(random_val)) + else: + replace_what = "" + else: + replace_what = self.config.replace_what + + # Apply replace where mask is True + for i in range(len(data_arr)): + if mask_arr[i]: + val = data_arr[i] + if val is not None and isinstance(val, str): + data_arr[i] = val.replace(replace_what, self.config.replace_with) - series.loc[series_mask] = series.loc[series_mask].apply(lambda x: x.replace(self.config.replace_what, self.config.replace_with)) - return series + return new_series_like(data, column, data_arr) diff --git a/tab_err/error_type/_typo.py b/tab_err/error_type/_typo.py index 1f690db..8f516c8 100644 --- a/tab_err/error_type/_typo.py +++ b/tab_err/error_type/_typo.py @@ -3,15 +3,13 @@ import random from typing import TYPE_CHECKING -from pandas.api.types import is_string_dtype +if TYPE_CHECKING: + import narwhals as nw -from tab_err._utils import get_column +from tab_err._utils import get_column, is_string_dtype, new_series_like, select_string_columns from ._error_type import ErrorType -if TYPE_CHECKING: - import pandas as pd - class Typo(ErrorType): """Inserts realistic typos into a column containing strings. @@ -26,37 +24,44 @@ class Typo(ErrorType): """ @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: series = get_column(data, column) if not is_string_dtype(series): msg = f"Column {column} does not contain values of the string dtype. Cannot apply Typos." raise TypeError(msg) - def _get_valid_columns(self: Typo, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: Typo, data: nw.DataFrame) -> list[str | int]: """Returns column names with string dtype elements.""" - return data.select_dtypes(include=["string", "object"]).columns.to_list() + return select_string_columns(data) - def _apply(self: Typo, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: Typo, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the Typo ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. typo_error_period: specifies how frequent typo corruptions are - see class description for details. Returns: - pd.Series: The data column, 'column', after Typo errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Typo errors at the locations specified by 'error_mask' are introduced. """ - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) - def butterfn(x: str) -> str: - return typo(x, self.config.typo_error_period, self.config.typo_keyboard_layout) + # Get numpy arrays + data_arr = series.to_numpy().copy() + mask_arr = series_mask.to_numpy() + + # Apply typo function to masked elements + for i in range(len(data_arr)): + if mask_arr[i]: + val = data_arr[i] + if val is not None and isinstance(val, str): + data_arr[i] = typo(val, self.config.typo_error_period, self.config.typo_keyboard_layout) - series.loc[series_mask] = series.loc[series_mask].apply(butterfn) - return series + return new_series_like(data, column, data_arr) def typo(input_text: str, typo_error_period: int = 10, layout: str = "ansi-qwerty") -> str: diff --git a/tab_err/error_type/_wrong_unit.py b/tab_err/error_type/_wrong_unit.py index 8a609e3..b247f95 100644 --- a/tab_err/error_type/_wrong_unit.py +++ b/tab_err/error_type/_wrong_unit.py @@ -3,49 +3,57 @@ import warnings from typing import TYPE_CHECKING -from pandas.api.types import is_numeric_dtype +if TYPE_CHECKING: + import narwhals as nw -from tab_err._utils import get_column +import numpy as np -from ._error_type import ErrorType +from tab_err._utils import get_column, is_numeric_dtype, new_series_like, select_numeric_columns -if TYPE_CHECKING: - import pandas as pd +from ._error_type import ErrorType class WrongUnit(ErrorType): """Simulate a column containing values that are scaled because they are not stored in the same unit.""" @staticmethod - def _check_type(data: pd.DataFrame, column: int | str) -> None: + def _check_type(data: nw.DataFrame, column: int | str) -> None: series = get_column(data, column) if not is_numeric_dtype(series): msg = f"Column {column} with dtype: {series.dtype} does not contain scalars. Cannot apply a wrong unit." raise TypeError(msg) - def _get_valid_columns(self: WrongUnit, data: pd.DataFrame) -> list[str | int]: + def _get_valid_columns(self: WrongUnit, data: nw.DataFrame) -> list[str | int]: """Returns all column names with numeric dtype elements.""" - return data.select_dtypes(include=["number"]).columns.tolist() + return select_numeric_columns(data) - def _apply(self: WrongUnit, data: pd.DataFrame, error_mask: pd.DataFrame, column: int | str) -> pd.Series: + def _apply(self: WrongUnit, data: nw.DataFrame, error_mask: nw.DataFrame, column: int | str) -> nw.Series: """Applies the WrongUnit ErrorType to a column of data. Args: - data (pd.DataFrame): DataFrame containing the column to add errors to. - error_mask (pd.DataFrame): A Pandas DataFrame with the same index & columns as 'data' that will be modified and returned. + data (nw.DataFrame): DataFrame containing the column to add errors to. + error_mask (nw.DataFrame): A DataFrame with the same index & columns as 'data' that will be modified and returned. column (int | str): The column of 'data' to create an error mask for. Returns: - pd.Series: The data column, 'column', after Replace errors at the locations specified by 'error_mask' are introduced. + nw.Series: The data column, 'column', after Replace errors at the locations specified by 'error_mask' are introduced. """ if self.config.wrong_unit_scaling is None: msg = "No scaling function was supplied for WrongUnit, defaulting to multiplication by 10.0." warnings.warn(msg, stacklevel=2) self.config.wrong_unit_scaling = lambda x: 10.0 * x - series = get_column(data, column).copy() + series = get_column(data, column) series_mask = get_column(error_mask, column) - series.loc[series_mask] = series.loc[series_mask].apply(self.config.wrong_unit_scaling) - return series + # Get numpy arrays + data_arr = series.to_numpy().copy().astype(np.float64) + mask_arr = series_mask.to_numpy() + + # Apply scaling function where mask is True + for i in range(len(data_arr)): + if mask_arr[i]: + data_arr[i] = self.config.wrong_unit_scaling(data_arr[i]) + + return new_series_like(data, column, data_arr) diff --git a/tests/api/test_backend_parity.py b/tests/api/test_backend_parity.py new file mode 100644 index 0000000..36b8549 --- /dev/null +++ b/tests/api/test_backend_parity.py @@ -0,0 +1,430 @@ +"""Tests to verify pandas and polars produce identical outputs with the same seed.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import narwhals as nw +import numpy as np +import pandas as pd +import polars as pl +import pytest + +from tab_err import ErrorModel, error_mechanism, error_type +from tab_err.api import MidLevelConfig, low_level, mid_level +from tab_err.api.high_level import create_errors as high_level_create_errors +from tests.conftest import assert_dataframes_equal + +if TYPE_CHECKING: + from narwhals.typing import IntoDataFrame + + +def _mask_sum(df: IntoDataFrame) -> int: + """Sum all boolean values in a mask DataFrame.""" + df_nw = nw.from_native(df, eager_only=True) + total = 0 + for col in df_nw.columns: + total += int(df_nw[col].to_numpy().sum()) + return total + + +def _column_all(df: IntoDataFrame, column: str) -> bool: + """Check if all values in a column are True.""" + df_nw = nw.from_native(df, eager_only=True) + return bool(df_nw[column].to_numpy().all()) + + +class TestLowLevelBackendParity: + """Tests for low-level API backend parity.""" + + @pytest.mark.parametrize("error_rate", [0.0, 0.1, 0.5, 1.0]) + @pytest.mark.parametrize("seed", [42, 123, 999]) + def test_ecar_add_delta_parity(self, error_rate: float, seed: int) -> None: + """Test ECAR + AddDelta produces identical results for pandas and polars.""" + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = low_level.create_errors(pd_df, "A", error_rate, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + pl_result, pl_mask = low_level.create_errors(pl_df, "A", error_rate, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + @pytest.mark.parametrize( + ("mechanism_cls", "error_type_cls", "column"), + [ + (error_mechanism.ECAR, error_type.AddDelta, "A"), # numeric column + (error_mechanism.ECAR, error_type.Outlier, "A"), # numeric column + (error_mechanism.ECAR, error_type.Typo, "C"), # string column + (error_mechanism.ECAR, error_type.MissingValue, "A"), # any column + (error_mechanism.ECAR, error_type.Replace, "C"), # string column + (error_mechanism.ENAR, error_type.AddDelta, "A"), # numeric column + ], + ) + def test_error_type_mechanism_combinations( + self, + mechanism_cls: type, + error_type_cls: type, + column: str, + ) -> None: + """Test various error type and mechanism combinations produce identical results.""" + seed = 42 + error_rate = 0.3 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = low_level.create_errors(pd_df, column, error_rate, mechanism_cls(seed=seed), error_type_cls(seed=seed)) + pl_result, pl_mask = low_level.create_errors(pl_df, column, error_rate, mechanism_cls(seed=seed), error_type_cls(seed=seed)) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + def test_category_swap_parity(self) -> None: + """Test CategorySwap produces identical results for pandas and polars with Categorical dtype.""" + seed = 42 + error_rate = 0.3 + rng = np.random.default_rng(42) + choices = rng.choice(["X", "Y", "Z"], 10).tolist() + + # Create pandas DataFrame with categorical dtype + pd_df = pd.DataFrame( + { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": pd.Categorical(choices), + } + ) + + # Create polars DataFrame with categorical dtype + pl_df = pl.DataFrame( + { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": pl.Series(choices).cast(pl.Categorical), + } + ) + + _pd_result, pd_mask = low_level.create_errors(pd_df, "C", error_rate, error_mechanism.ECAR(seed=seed), error_type.CategorySwap(seed=seed)) + _pl_result, pl_mask = low_level.create_errors(pl_df, "C", error_rate, error_mechanism.ECAR(seed=seed), error_type.CategorySwap(seed=seed)) + + assert_dataframes_equal(pd_mask, pl_mask) + + def test_ear_mechanism_parity(self) -> None: + """Test EAR conditioned on another column produces identical results.""" + seed = 42 + error_rate = 0.3 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = low_level.create_errors( + pd_df, "A", error_rate, error_mechanism.EAR(condition_to_column="B", seed=seed), error_type.AddDelta(seed=seed) + ) + pl_result, pl_mask = low_level.create_errors( + pl_df, "A", error_rate, error_mechanism.EAR(condition_to_column="B", seed=seed), error_type.AddDelta(seed=seed) + ) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + +class TestMidLevelBackendParity: + """Tests for mid-level API backend parity.""" + + def test_basic_config_parity(self) -> None: + """Test single error model per column produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + config = MidLevelConfig( + columns={ + "A": [ErrorModel(error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed), error_rate=0.3)], + } + ) + + pd_result, pd_mask = mid_level.create_errors(pd_df, config) + pl_result, pl_mask = mid_level.create_errors(pl_df, config) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + def test_multiple_models_per_column_parity(self) -> None: + """Test multiple error models on same column produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + config = MidLevelConfig( + columns={ + "A": [ + ErrorModel(error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed), error_rate=0.2), + ErrorModel(error_mechanism.ECAR(seed=seed + 1), error_type.Outlier(seed=seed + 1), error_rate=0.2), + ], + } + ) + + pd_result, pd_mask = mid_level.create_errors(pd_df, config) + pl_result, pl_mask = mid_level.create_errors(pl_df, config) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + +class TestHighLevelBackendParity: + """Tests for high-level API backend parity.""" + + @pytest.mark.parametrize("error_rate", [0.1, 0.5]) + @pytest.mark.parametrize("n_models", [1, 2]) + def test_automatic_error_generation_parity(self, error_rate: float, n_models: int) -> None: + """Test automatic error generation produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 100).tolist(), + "B": rng.random(100).tolist(), + "C": rng.choice(["X", "Y", "Z"], 100).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = high_level_create_errors(pd_df, error_rate, n_error_models_per_column=n_models, seed=seed) + pl_result, pl_mask = high_level_create_errors(pl_df, error_rate, n_error_models_per_column=n_models, seed=seed) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + def test_with_error_type_filters_parity(self) -> None: + """Test include/exclude error type filters produce identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 100).tolist(), + "B": rng.random(100).tolist(), + "C": rng.choice(["X", "Y", "Z"], 100).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + # Test with include filter + pd_result, pd_mask = high_level_create_errors( + pd_df, + error_rate=0.3, + error_types_to_include=[error_type.AddDelta(seed=seed), error_type.MissingValue(seed=seed)], + seed=seed, + ) + pl_result, pl_mask = high_level_create_errors( + pl_df, + error_rate=0.3, + error_types_to_include=[error_type.AddDelta(seed=seed), error_type.MissingValue(seed=seed)], + seed=seed, + ) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + def test_with_mechanism_filters_parity(self) -> None: + """Test include/exclude mechanism filters produce identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 100).tolist(), + "B": rng.random(100).tolist(), + "C": rng.choice(["X", "Y", "Z"], 100).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + # Test with mechanism exclude filter + pd_result, pd_mask = high_level_create_errors( + pd_df, + error_rate=0.3, + error_mechanisms_to_exclude=[error_mechanism.EAR(condition_to_column=None)], + seed=seed, + ) + pl_result, pl_mask = high_level_create_errors( + pl_df, + error_rate=0.3, + error_mechanisms_to_exclude=[error_mechanism.EAR(condition_to_column=None)], + seed=seed, + ) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + def test_datetime_column_parity(self) -> None: + """Test datetime column handling produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": pd.date_range(start="2025-03-04", periods=10, freq="2h").tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df = pd.DataFrame(raw) + pl_df = pl.DataFrame(raw) + + pd_result, pd_mask = high_level_create_errors(pd_df, error_rate=0.3, seed=seed) + pl_result, pl_mask = high_level_create_errors(pl_df, error_rate=0.3, seed=seed) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + +class TestReturnTypePreservation: + """Tests verifying input type is preserved in output.""" + + def test_pandas_input_returns_pandas(self) -> None: + """Verify pandas input returns pandas output.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df = pd.DataFrame(raw) + + # Low-level API + result, mask = low_level.create_errors(pd_df, "A", 0.3, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + assert isinstance(result, pd.DataFrame) + assert isinstance(mask, pd.DataFrame) + + # Mid-level API + config = MidLevelConfig(columns={"A": [ErrorModel(error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed), error_rate=0.3)]}) + result, mask = mid_level.create_errors(pd_df, config) + assert isinstance(result, pd.DataFrame) + assert isinstance(mask, pd.DataFrame) + + # High-level API + result, mask = high_level_create_errors(pd_df, error_rate=0.3, seed=seed) + assert isinstance(result, pd.DataFrame) + assert isinstance(mask, pd.DataFrame) + + def test_polars_input_returns_polars(self) -> None: + """Verify polars input returns polars output.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pl_df = pl.DataFrame(raw) + + # Low-level API + result, mask = low_level.create_errors(pl_df, "A", 0.3, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + assert isinstance(result, pl.DataFrame) + assert isinstance(mask, pl.DataFrame) + + # Mid-level API + config = MidLevelConfig(columns={"A": [ErrorModel(error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed), error_rate=0.3)]}) + result, mask = mid_level.create_errors(pl_df, config) + assert isinstance(result, pl.DataFrame) + assert isinstance(mask, pl.DataFrame) + + # High-level API + result, mask = high_level_create_errors(pl_df, error_rate=0.3, seed=seed) + assert isinstance(result, pl.DataFrame) + assert isinstance(mask, pl.DataFrame) + + +class TestEdgeCases: + """Tests for edge cases in backend parity.""" + + def test_zero_error_rate_parity(self) -> None: + """Test 0% error rate produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = low_level.create_errors(pd_df, "A", 0.0, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + pl_result, pl_mask = low_level.create_errors(pl_df, "A", 0.0, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + # Verify no errors were introduced + assert _mask_sum(pd_mask) == 0 + assert _mask_sum(pl_mask) == 0 + + def test_full_error_rate_parity(self) -> None: + """Test 100% error rate produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = low_level.create_errors(pd_df, "A", 1.0, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + pl_result, pl_mask = low_level.create_errors(pl_df, "A", 1.0, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + # Verify all rows in column A have errors + assert _column_all(pd_mask, "A") + assert _column_all(pl_mask, "A") + + def test_large_dataset_parity(self) -> None: + """Test 10,000 rows produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10000).tolist(), + "B": rng.random(10000).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10000).tolist(), + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = low_level.create_errors(pd_df, "A", 0.3, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + pl_result, pl_mask = low_level.create_errors(pl_df, "A", 0.3, error_mechanism.ECAR(seed=seed), error_type.AddDelta(seed=seed)) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) + + def test_empty_string_handling_parity(self) -> None: + """Test empty strings in string columns produces identical results.""" + seed = 42 + rng = np.random.default_rng(42) + raw = { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": ["", "X", "", "Y", "Z", "", "X", "Y", "", "Z"], + } + pd_df, pl_df = pd.DataFrame(raw), pl.DataFrame(raw) + + pd_result, pd_mask = low_level.create_errors(pd_df, "C", 0.3, error_mechanism.ECAR(seed=seed), error_type.Typo(seed=seed)) + pl_result, pl_mask = low_level.create_errors(pl_df, "C", 0.3, error_mechanism.ECAR(seed=seed), error_type.Typo(seed=seed)) + + assert_dataframes_equal(pd_result, pl_result) + assert_dataframes_equal(pd_mask, pl_mask) diff --git a/tests/api/test_dtype_preservation.py b/tests/api/test_dtype_preservation.py new file mode 100644 index 0000000..21bb795 --- /dev/null +++ b/tests/api/test_dtype_preservation.py @@ -0,0 +1,81 @@ +"""Tests to verify dtype preservation after error application.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import narwhals as nw +import numpy as np +import pandas as pd +import polars as pl +import pytest + +if TYPE_CHECKING: + from narwhals.typing import IntoDataFrame + +from tab_err import error_mechanism, error_type +from tab_err.api import low_level + + +def _nw_dtype(df: IntoDataFrame, column: str) -> object: + return nw.from_native(df, eager_only=True)[column].dtype + + +def _assert_dtype_preserved(original: IntoDataFrame, mutated: IntoDataFrame, column: str) -> None: + assert _nw_dtype(original, column) == _nw_dtype(mutated, column) + + +@pytest.mark.parametrize("backend", ["pandas", "polars"]) +def test_categorical_dtype_preserved(backend: str) -> None: + """Ensure categorical dtype is preserved after error injection.""" + raw = {"cat": ["a", "b", "a", "c"], "num": [1, 2, 3, 4]} + if backend == "pandas": + df = pd.DataFrame(raw) + df["cat"] = pd.Categorical(df["cat"]) + else: + df = pl.DataFrame(raw).with_columns(pl.col("cat").cast(pl.Categorical)) + + mutated, _mask = low_level.create_errors( + df, + "cat", + error_rate=0.5, + error_mechanism=error_mechanism.ECAR(seed=0), + error_type=error_type.CategorySwap(seed=0), + ) + + _assert_dtype_preserved(df, mutated, "cat") + + +@pytest.mark.parametrize("backend", ["pandas", "polars"]) +def test_datetime_dtype_preserved(backend: str) -> None: + """Ensure datetime dtype is preserved after error injection.""" + ts = np.array(["2024-01-01", "2024-01-02", "2024-01-03"], dtype="datetime64[ns]") + raw = {"ts": ts, "num": [1, 2, 3]} + df = pd.DataFrame(raw) if backend == "pandas" else pl.DataFrame(raw) + + mutated, _mask = low_level.create_errors( + df, + "ts", + error_rate=0.5, + error_mechanism=error_mechanism.ECAR(seed=0), + error_type=error_type.AddDelta(config={"add_delta_value": 3600}, seed=0), + ) + + _assert_dtype_preserved(df, mutated, "ts") + + +@pytest.mark.parametrize("backend", ["pandas", "polars"]) +def test_nullable_int_dtype_preserved(backend: str) -> None: + """Ensure nullable integer dtype is preserved after error injection.""" + raw = {"x": [1, 2, 3, 4]} + df = pd.DataFrame({"x": pd.Series(raw["x"], dtype="Int64")}) if backend == "pandas" else pl.DataFrame({"x": pl.Series(raw["x"], dtype=pl.Int64)}) + + mutated, _mask = low_level.create_errors( + df, + "x", + error_rate=0.5, + error_mechanism=error_mechanism.ECAR(seed=0), + error_type=error_type.AddDelta(config={"add_delta_value": 1}, seed=0), + ) + + _assert_dtype_preserved(df, mutated, "x") diff --git a/tests/api/test_high_level_api.py b/tests/api/test_high_level_api.py index ad062a7..61d1f7e 100644 --- a/tests/api/test_high_level_api.py +++ b/tests/api/test_high_level_api.py @@ -1,41 +1,74 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import narwhals as nw import numpy as np -import pandas as pd import pytest from tab_err.api.high_level import create_errors +from tests.conftest import assert_dataframes_equal + +if TYPE_CHECKING: + from narwhals.typing import IntoDataFrame + + +def _get_shape(df: IntoDataFrame) -> tuple[int, int]: + """Get shape of a DataFrame using narwhals.""" + return nw.from_native(df, eager_only=True).shape + + +def _get_numpy_mean(df: IntoDataFrame) -> float: + """Get numpy array mean from a DataFrame using narwhals.""" + df_nw = nw.from_native(df, eager_only=True) + all_values = [] + for col in df_nw.columns: + all_values.extend(df_nw[col].to_numpy().tolist()) + return float(np.mean(all_values)) + + +def _check_all_bool_dtypes(df: IntoDataFrame) -> bool: + """Check if all columns in a DataFrame have boolean dtype.""" + df_nw = nw.from_native(df, eager_only=True) + return all(df_nw[col].dtype == nw.Boolean for col in df_nw.columns) + + +def _check_same_type(df1: IntoDataFrame, df2: IntoDataFrame) -> bool: + """Check if two DataFrames are of the same type.""" + return type(df1) is type(df2) class TestHighLevelAPI: """Tests the high-level API.""" - def test_create_errors_basic(self, test_data: dict[str, pd.DataFrame]) -> None: + def test_create_errors_basic(self, test_data: dict[str, IntoDataFrame]) -> None: """Test that create_errors returns two DataFrames with expected properties.""" seed = 42 error_rate = 0.5 modified_data_4rows_5columns, data_4rows_5columns_error_mask = create_errors(test_data["data_4rows_5columns"], error_rate, seed=seed) modified_data_10rows_3columns, data_10rows_3columns_error_mask = create_errors(test_data["data_10rows_3columns"], error_rate, seed=seed) - # Check that they are still dataframes - assert isinstance(modified_data_4rows_5columns, pd.DataFrame) - assert isinstance(data_4rows_5columns_error_mask, pd.DataFrame) - assert isinstance(modified_data_10rows_3columns, pd.DataFrame) - assert isinstance(data_10rows_3columns_error_mask, pd.DataFrame) + # Check that the output type matches the input type + assert _check_same_type(modified_data_4rows_5columns, test_data["data_4rows_5columns"]) + assert _check_same_type(data_4rows_5columns_error_mask, test_data["data_4rows_5columns"]) + assert _check_same_type(modified_data_10rows_3columns, test_data["data_10rows_3columns"]) + assert _check_same_type(data_10rows_3columns_error_mask, test_data["data_10rows_3columns"]) # Check Shapes - assert modified_data_4rows_5columns.shape == test_data["data_4rows_5columns"].shape - assert data_4rows_5columns_error_mask.shape == test_data["data_4rows_5columns"].shape - assert modified_data_10rows_3columns.shape == test_data["data_10rows_3columns"].shape - assert data_10rows_3columns_error_mask.shape == test_data["data_10rows_3columns"].shape + assert _get_shape(modified_data_4rows_5columns) == _get_shape(test_data["data_4rows_5columns"]) + assert _get_shape(data_4rows_5columns_error_mask) == _get_shape(test_data["data_4rows_5columns"]) + assert _get_shape(modified_data_10rows_3columns) == _get_shape(test_data["data_10rows_3columns"]) + assert _get_shape(data_10rows_3columns_error_mask) == _get_shape(test_data["data_10rows_3columns"]) # Assert the error masks contain only boolean values - assert data_4rows_5columns_error_mask.dtypes.apply(lambda dt: np.issubdtype(dt, np.bool_)).all() - assert data_10rows_3columns_error_mask.dtypes.apply(lambda dt: np.issubdtype(dt, np.bool_)).all() + assert _check_all_bool_dtypes(data_4rows_5columns_error_mask) + assert _check_all_bool_dtypes(data_10rows_3columns_error_mask) # Assert that the error masks have the correct proportion of True to False - assert pytest.approx(error_rate) == data_4rows_5columns_error_mask.to_numpy().mean() - assert pytest.approx(error_rate) == data_10rows_3columns_error_mask.to_numpy().mean() + assert pytest.approx(error_rate) == _get_numpy_mean(data_4rows_5columns_error_mask) + assert pytest.approx(error_rate) == _get_numpy_mean(data_10rows_3columns_error_mask) - def test_create_errors_seed(self, test_data: dict[str, pd.DataFrame]) -> None: + def test_create_errors_seed(self, test_data: dict[str, IntoDataFrame]) -> None: """Test that create_errors returns the same dataframe when a seed is used.""" seed = 42 error_rate = 0.5 @@ -44,10 +77,10 @@ def test_create_errors_seed(self, test_data: dict[str, pd.DataFrame]) -> None: modified_data_2, error_mask_2 = create_errors(test_data["data_10rows_3columns"], error_rate=error_rate, seed=seed) # Ensure same seed yields same dataframes - pd.testing.assert_frame_equal(modified_data_1, modified_data_2) - pd.testing.assert_frame_equal(error_mask_1, error_mask_2) + assert_dataframes_equal(modified_data_1, modified_data_2) + assert_dataframes_equal(error_mask_1, error_mask_2) - def test_create_errors_error_rates(self, test_data: dict[str, pd.DataFrame]) -> None: + def test_create_errors_error_rates(self, test_data: dict[str, IntoDataFrame]) -> None: """Test that create_errors returns two DataFrames with expected properties.""" seed = 42 for i in range(11): @@ -57,11 +90,11 @@ def test_create_errors_error_rates(self, test_data: dict[str, pd.DataFrame]) -> _, data_10rows_3columns_with_datetime_error_mask = create_errors(test_data["data_10rows_3columns_with_datetime"], error_rate, seed=seed) # Assert that the error masks have the correct proportion of True to False - assert pytest.approx(error_rate) == data_100rows_3columns_error_mask.to_numpy().mean() - assert pytest.approx(error_rate) == data_10rows_3columns_error_mask.to_numpy().mean() - assert pytest.approx(error_rate) == data_10rows_3columns_with_datetime_error_mask.to_numpy().mean() + assert pytest.approx(error_rate) == _get_numpy_mean(data_100rows_3columns_error_mask) + assert pytest.approx(error_rate) == _get_numpy_mean(data_10rows_3columns_error_mask) + assert pytest.approx(error_rate) == _get_numpy_mean(data_10rows_3columns_with_datetime_error_mask) - def test_create_errors_more_models(self, test_data: dict[str, pd.DataFrame]) -> None: + def test_create_errors_more_models(self, test_data: dict[str, IntoDataFrame]) -> None: """Test that when more error models are introduced, the create_errors method has expected DataFrame return.""" error_rate = 1.0 seed = 42 @@ -77,28 +110,28 @@ def test_create_errors_more_models(self, test_data: dict[str, pd.DataFrame]) -> test_data["data_100rows_3columns"], error_rate, n_error_models_per_column=n_error_models, seed=seed ) - # Check that they are still dataframes - assert isinstance(modified_data_4rows_5columns, pd.DataFrame) - assert isinstance(data_4rows_5columns_error_mask, pd.DataFrame) - assert isinstance(modified_data_10rows_3columns, pd.DataFrame) - assert isinstance(data_10rows_3columns_error_mask, pd.DataFrame) - assert isinstance(modified_data_100rows_3columns, pd.DataFrame) - assert isinstance(data_100rows_3columns_error_mask, pd.DataFrame) + # Check that the output type matches the input type + assert _check_same_type(modified_data_4rows_5columns, test_data["data_4rows_5columns"]) + assert _check_same_type(data_4rows_5columns_error_mask, test_data["data_4rows_5columns"]) + assert _check_same_type(modified_data_10rows_3columns, test_data["data_10rows_3columns"]) + assert _check_same_type(data_10rows_3columns_error_mask, test_data["data_10rows_3columns"]) + assert _check_same_type(modified_data_100rows_3columns, test_data["data_100rows_3columns"]) + assert _check_same_type(data_100rows_3columns_error_mask, test_data["data_100rows_3columns"]) # Check Shapes - assert modified_data_4rows_5columns.shape == test_data["data_4rows_5columns"].shape - assert data_4rows_5columns_error_mask.shape == test_data["data_4rows_5columns"].shape - assert modified_data_10rows_3columns.shape == test_data["data_10rows_3columns"].shape - assert data_10rows_3columns_error_mask.shape == test_data["data_10rows_3columns"].shape - assert modified_data_100rows_3columns.shape == test_data["data_100rows_3columns"].shape - assert data_100rows_3columns_error_mask.shape == test_data["data_100rows_3columns"].shape + assert _get_shape(modified_data_4rows_5columns) == _get_shape(test_data["data_4rows_5columns"]) + assert _get_shape(data_4rows_5columns_error_mask) == _get_shape(test_data["data_4rows_5columns"]) + assert _get_shape(modified_data_10rows_3columns) == _get_shape(test_data["data_10rows_3columns"]) + assert _get_shape(data_10rows_3columns_error_mask) == _get_shape(test_data["data_10rows_3columns"]) + assert _get_shape(modified_data_100rows_3columns) == _get_shape(test_data["data_100rows_3columns"]) + assert _get_shape(data_100rows_3columns_error_mask) == _get_shape(test_data["data_100rows_3columns"]) # Assert the error masks contain only boolean values - assert data_4rows_5columns_error_mask.dtypes.apply(lambda dt: np.issubdtype(dt, np.bool_)).all() - assert data_10rows_3columns_error_mask.dtypes.apply(lambda dt: np.issubdtype(dt, np.bool_)).all() - assert data_100rows_3columns_error_mask.dtypes.apply(lambda dt: np.issubdtype(dt, np.bool_)).all() + assert _check_all_bool_dtypes(data_4rows_5columns_error_mask) + assert _check_all_bool_dtypes(data_10rows_3columns_error_mask) + assert _check_all_bool_dtypes(data_100rows_3columns_error_mask) # Assert that the error masks have the correct proportion of True to False - assert pytest.approx(error_rate) == data_4rows_5columns_error_mask.to_numpy().mean() - assert pytest.approx(error_rate) == data_10rows_3columns_error_mask.to_numpy().mean() - assert pytest.approx(error_rate) == data_100rows_3columns_error_mask.to_numpy().mean() + assert pytest.approx(error_rate) == _get_numpy_mean(data_4rows_5columns_error_mask) + assert pytest.approx(error_rate) == _get_numpy_mean(data_10rows_3columns_error_mask) + assert pytest.approx(error_rate) == _get_numpy_mean(data_100rows_3columns_error_mask) diff --git a/tests/api/test_low_level_api.py b/tests/api/test_low_level_api.py index f4bc3d0..f2c70a8 100644 --- a/tests/api/test_low_level_api.py +++ b/tests/api/test_low_level_api.py @@ -1,14 +1,31 @@ -import pandas as pd +from __future__ import annotations + +from typing import TYPE_CHECKING + +import narwhals as nw +import numpy as np import pytest from tab_err import error_mechanism, error_type from tab_err.api.low_level import create_errors +if TYPE_CHECKING: + from narwhals.typing import IntoDataFrame + + +def _get_numpy_mean(df: IntoDataFrame) -> float: + """Get numpy array mean from a DataFrame using narwhals.""" + df_nw = nw.from_native(df, eager_only=True) + all_values = [] + for col in df_nw.columns: + all_values.extend(df_nw[col].to_numpy().tolist()) + return float(np.mean(all_values)) + class TestLowLevelAPI: """Tests the low-level API.""" - def test_create_errors_error_rates(self, test_data: dict[str, pd.DataFrame]) -> None: + def test_create_errors_error_rates(self, test_data: dict[str, IntoDataFrame]) -> None: """Test that create_errors returns two DataFrames with expected properties.""" for i in range(11): error_rate = 0.1 * float(i) @@ -20,5 +37,5 @@ def test_create_errors_error_rates(self, test_data: dict[str, pd.DataFrame]) -> ) # Assert that the error masks have the correct proportion of True to False - Note only one column is errored - assert pytest.approx(error_rate / 3.0) == data_100rows_3columns_error_mask.to_numpy().mean() - assert pytest.approx(error_rate / 3.0) == data_10rows_3columns_error_mask.to_numpy().mean() + assert pytest.approx(error_rate / 3.0) == _get_numpy_mean(data_100rows_3columns_error_mask) + assert pytest.approx(error_rate / 3.0) == _get_numpy_mean(data_10rows_3columns_error_mask) diff --git a/tests/conftest.py b/tests/conftest.py index f48207f..6f56b3a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,19 +1,86 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import narwhals as nw import numpy as np import pandas as pd +import polars as pl import pytest +if TYPE_CHECKING: + from narwhals.typing import IntoDataFrame + from numpy.random import Generator -@pytest.fixture -def test_data() -> dict[str, pd.DataFrame]: - """Fixture to provide test data before each test runs.""" - rng = np.random.default_rng(42) + +def _create_raw_test_data(rng: Generator) -> dict[str, dict]: + """Create raw data dicts that can be converted to any backend. + + Args: + rng: NumPy random generator for reproducible data generation. + + Returns: + Dictionary mapping dataset names to column data dictionaries. + """ return { - "data_10rows_3columns": pd.DataFrame({"A": rng.integers(0, 100, 10), "B": rng.random(10), "C": rng.choice(["X", "Y", "Z"], 10)}), - "data_4rows_5columns": pd.DataFrame( - {"A": rng.integers(0, 100, 4), "B": rng.random(4), "C": rng.choice(["X", "Y", "Z"], 4), "D": rng.integers(0, 100, 4), "E": rng.random(4)} - ), - "data_100rows_3columns": pd.DataFrame({"A": rng.integers(0, 100, 100), "B": rng.random(100), "C": rng.choice(["X", "Y", "Z"], 100)}), - "data_10rows_3columns_with_datetime": pd.DataFrame( - {"A": rng.integers(0, 100, 10), "B": pd.date_range(start="2025-03-04", periods=10, freq="2h"), "C": rng.choice(["X", "Y", "Z"], 10)} - ), + "data_10rows_3columns": { + "A": rng.integers(0, 100, 10).tolist(), + "B": rng.random(10).tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + }, + "data_4rows_5columns": { + "A": rng.integers(0, 100, 4).tolist(), + "B": rng.random(4).tolist(), + "C": rng.choice(["X", "Y", "Z"], 4).tolist(), + "D": rng.integers(0, 100, 4).tolist(), + "E": rng.random(4).tolist(), + }, + "data_100rows_3columns": { + "A": rng.integers(0, 100, 100).tolist(), + "B": rng.random(100).tolist(), + "C": rng.choice(["X", "Y", "Z"], 100).tolist(), + }, + "data_10rows_3columns_with_datetime": { + "A": rng.integers(0, 100, 10).tolist(), + "B": pd.date_range(start="2025-03-04", periods=10, freq="2h").tolist(), + "C": rng.choice(["X", "Y", "Z"], 10).tolist(), + }, } + + +def assert_dataframes_equal( + df1: IntoDataFrame, + df2: IntoDataFrame, + rtol: float = 1e-7, + atol: float = 1e-10, +) -> None: + """Compare DataFrames by converting to pandas and using pd.testing.assert_frame_equal. + + Args: + df1: First DataFrame (any narwhals-compatible DataFrame). + df2: Second DataFrame (any narwhals-compatible DataFrame). + rtol: Relative tolerance for numeric comparisons. + atol: Absolute tolerance for numeric comparisons. + + Raises: + AssertionError: If DataFrames are not equal. + """ + # Use narwhals to wrap and then get native back for conversion + df1_native = nw.from_native(df1, eager_only=True).to_pandas() + df2_native = nw.from_native(df2, eager_only=True).to_pandas() + + pd.testing.assert_frame_equal(df1_native, df2_native, check_dtype=False, rtol=rtol, atol=atol) + + +@pytest.fixture(params=["pandas", "polars"]) +def test_data(request: pytest.FixtureRequest) -> dict[str, IntoDataFrame]: + """Fixture to provide test data before each test runs. + + Parametrized to run tests with both pandas and polars backends. + """ + rng = np.random.default_rng(42) + raw_data = _create_raw_test_data(rng) + + if request.param == "pandas": + return {k: pd.DataFrame(v) for k, v in raw_data.items()} + return {k: pl.DataFrame(v) for k, v in raw_data.items()} diff --git a/tests/error_type/test_typo.py b/tests/error_type/test_typo.py index 1032e79..575e1fa 100644 --- a/tests/error_type/test_typo.py +++ b/tests/error_type/test_typo.py @@ -1,3 +1,5 @@ +from typing import cast + import pandas as pd from tab_err import error_mechanism, error_type @@ -12,4 +14,5 @@ def test_typo() -> None: } ) modified_df, _ = create_errors(test_data, "A", 1, error_mechanism.ECAR(), error_type.Typo()) - assert modified_df.iloc[0, 0] != "" + modified_df_pd = cast("pd.DataFrame", modified_df) + assert modified_df_pd.iloc[0, 0] != "" diff --git a/uv.lock b/uv.lock index 82ce954..da64b5e 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10, <3.15" resolution-markers = [ "python_full_version >= '3.14'", @@ -31,16 +31,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.12.0" +version = "4.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "idna" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] [[package]] @@ -131,15 +131,15 @@ wheels = [ [[package]] name = "astroid" -version = "4.0.2" +version = "4.0.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.12' and python_full_version < '3.14'", ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/22/97df040e15d964e592d3a180598ace67e91b7c559d8298bdb3c949dc6e42/astroid-4.0.2.tar.gz", hash = "sha256:ac8fb7ca1c08eb9afec91ccc23edbd8ac73bb22cbdd7da1d488d9fb8d6579070", size = 405714, upload-time = "2025-11-09T21:21:18.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/ca/c17d0f83016532a1ad87d1de96837164c99d47a3b6bbba28bd597c25b37a/astroid-4.0.3.tar.gz", hash = "sha256:08d1de40d251cc3dc4a7a12726721d475ac189e4e583d596ece7422bc176bda3", size = 406224, upload-time = "2026-01-03T22:14:26.096Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/ac/a85b4bfb4cf53221513e27f33cc37ad158fce02ac291d18bee6b49ab477d/astroid-4.0.2-py3-none-any.whl", hash = "sha256:d7546c00a12efc32650b19a2bb66a153883185d3179ab0d4868086f807338b9b", size = 276354, upload-time = "2025-11-09T21:21:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/ce/66/686ac4fc6ef48f5bacde625adac698f41d5316a9753c2b20bb0931c9d4e2/astroid-4.0.3-py3-none-any.whl", hash = "sha256:864a0a34af1bd70e1049ba1e61cee843a7252c826d97825fcee9b2fcbd9e1b14", size = 276443, upload-time = "2026-01-03T22:14:24.412Z" }, ] [[package]] @@ -153,14 +153,14 @@ wheels = [ [[package]] name = "async-lru" -version = "2.0.5" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c3/bbf34f15ea88dfb649ab2c40f9d75081784a50573a9ea431563cab64adb8/async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed", size = 12041, upload-time = "2026-01-17T22:52:18.931Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e9/eb6a5db5ac505d5d45715388e92bced7a5bb556facc4d0865d192823f2d2/async_lru-2.1.0-py3-none-any.whl", hash = "sha256:fa12dcf99a42ac1280bc16c634bbaf06883809790f6304d85cdab3f666f33a7e", size = 6933, upload-time = "2026-01-17T22:52:17.389Z" }, ] [[package]] @@ -213,11 +213,11 @@ css = [ [[package]] name = "certifi" -version = "2025.11.12" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -478,11 +478,28 @@ wheels = [ name = "docutils" version = "0.21.2" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, ] +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + [[package]] name = "exceptiongroup" version = "1.3.1" @@ -515,11 +532,11 @@ wheels = [ [[package]] name = "filelock" -version = "3.20.1" +version = "3.20.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, ] [[package]] @@ -570,11 +587,11 @@ wheels = [ [[package]] name = "identify" -version = "2.6.15" +version = "2.6.16" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] [[package]] @@ -612,8 +629,8 @@ dependencies = [ { name = "appnope", marker = "sys_platform == 'darwin'" }, { name = "comm" }, { name = "debugpy" }, - { name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "jupyter-client" }, { name = "jupyter-core" }, { name = "matplotlib-inline" }, @@ -631,7 +648,7 @@ wheels = [ [[package]] name = "ipython" -version = "8.37.0" +version = "8.38.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version < '3.11'", @@ -649,14 +666,14 @@ dependencies = [ { name = "traitlets", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/31/10ac88f3357fc276dc8a64e8880c82e80e7459326ae1d0a211b40abf6665/ipython-8.37.0.tar.gz", hash = "sha256:ca815841e1a41a1e6b73a0b08f3038af9b2252564d01fc405356d34033012216", size = 5606088, upload-time = "2025-05-31T16:39:09.613Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/91/d0/274fbf7b0b12643cbbc001ce13e6a5b1607ac4929d1b11c72460152c9fc3/ipython-8.37.0-py3-none-any.whl", hash = "sha256:ed87326596b878932dbcb171e3e698845434d8c61b8d8cd474bf663041a9dcf2", size = 831864, upload-time = "2025-05-31T16:39:06.38Z" }, + { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, ] [[package]] name = "ipython" -version = "9.8.0" +version = "9.9.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", @@ -676,9 +693,9 @@ dependencies = [ { name = "traitlets", marker = "python_full_version >= '3.11'" }, { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/12/51/a703c030f4928646d390b4971af4938a1b10c9dfce694f0d99a0bb073cb2/ipython-9.8.0.tar.gz", hash = "sha256:8e4ce129a627eb9dd221c41b1d2cdaed4ef7c9da8c17c63f6f578fe231141f83", size = 4424940, upload-time = "2025-12-03T10:18:24.353Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/dd/fb08d22ec0c27e73c8bc8f71810709870d51cadaf27b7ddd3f011236c100/ipython-9.9.0.tar.gz", hash = "sha256:48fbed1b2de5e2c7177eefa144aba7fcb82dac514f09b57e2ac9da34ddb54220", size = 4425043, upload-time = "2026-01-05T12:36:46.233Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/df/8ee1c5dd1e3308b5d5b2f2dfea323bb2f3827da8d654abb6642051199049/ipython-9.8.0-py3-none-any.whl", hash = "sha256:ebe6d1d58d7d988fbf23ff8ff6d8e1622cfdb194daf4b7b73b792c4ec3b85385", size = 621374, upload-time = "2025-12-03T10:18:22.335Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/162cfaee4ccf370465c5af1ce36a9eacec1becb552f2033bb3584e6f640a/ipython-9.9.0-py3-none-any.whl", hash = "sha256:b457fe9165df2b84e8ec909a97abcf2ed88f565970efba16b1f7229c283d252b", size = 621431, upload-time = "2026-01-05T12:36:44.669Z" }, ] [[package]] @@ -749,7 +766,7 @@ wheels = [ [[package]] name = "jsonschema" -version = "4.25.1" +version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -757,9 +774,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, ] [package.optional-dependencies] @@ -789,7 +806,7 @@ wheels = [ [[package]] name = "jupyter-client" -version = "8.7.0" +version = "8.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jupyter-core" }, @@ -798,9 +815,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/27/d10de45e8ad4ce872372c4a3a37b7b35b6b064f6f023a5c14ffcced4d59d/jupyter_client-8.7.0.tar.gz", hash = "sha256:3357212d9cbe01209e59190f67a3a7e1f387a4f4e88d1e0433ad84d7b262531d", size = 344691, upload-time = "2025-12-09T18:37:01.953Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl", hash = "sha256:3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0", size = 106215, upload-time = "2025-12-09T18:37:00.024Z" }, + { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, ] [[package]] @@ -879,20 +896,20 @@ wheels = [ [[package]] name = "jupyter-server-terminals" -version = "0.5.3" +version = "0.5.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pywinpty", marker = "os_name == 'nt'" }, { name = "terminado" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/a7/bcd0a9b0cbba88986fe944aaaf91bfda603e5a50bda8ed15123f381a3b2f/jupyter_server_terminals-0.5.4.tar.gz", hash = "sha256:bbda128ed41d0be9020349f9f1f2a4ab9952a73ed5f5ac9f1419794761fb87f5", size = 31770, upload-time = "2026-01-14T16:53:20.213Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656, upload-time = "2024-03-12T14:37:00.708Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2d/6674563f71c6320841fc300911a55143925112a72a883e2ca71fba4c618d/jupyter_server_terminals-0.5.4-py3-none-any.whl", hash = "sha256:55be353fc74a80bc7f3b20e6be50a55a61cd525626f578dcb66a5708e2007d14", size = 13704, upload-time = "2026-01-14T16:53:18.738Z" }, ] [[package]] name = "jupyterlab" -version = "4.5.1" +version = "4.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "async-lru" }, @@ -910,9 +927,9 @@ dependencies = [ { name = "tornado" }, { name = "traitlets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/21/413d142686a4e8f4268d985becbdb4daf060524726248e73be4773786987/jupyterlab-4.5.1.tar.gz", hash = "sha256:09da1ddfbd9eec18b5101dbb8515612aa1e47443321fb99503725a88e93d20d9", size = 23992251, upload-time = "2025-12-15T16:58:59.361Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/dc/2c8c4ff1aee27ac999ba04c373c5d0d7c6c181b391640d7b916b884d5985/jupyterlab-4.5.2.tar.gz", hash = "sha256:c80a6b9f6dace96a566d590c65ee2785f61e7cd4aac5b4d453dcc7d0d5e069b7", size = 23990371, upload-time = "2026-01-12T12:27:08.493Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/c3/acced767eecc11a70c65c45295db5396c4f0c1937874937d5a76d7b177b6/jupyterlab-4.5.1-py3-none-any.whl", hash = "sha256:31b059de96de0754ff1f2ce6279774b6aab8c34d7082e9752db58207c99bd514", size = 12384821, upload-time = "2025-12-15T16:58:55.563Z" }, + { url = "https://files.pythonhosted.org/packages/a4/78/7e455920f104ef2aa94a4c0d2b40e5b44334ee7057eae1aa1fb97b9631ad/jupyterlab-4.5.2-py3-none-any.whl", hash = "sha256:76466ebcfdb7a9bb7e2fbd6459c0e2c032ccf75be673634a84bee4b3e6b13ab6", size = 12385807, upload-time = "2026-01-12T12:27:03.923Z" }, ] [[package]] @@ -953,89 +970,109 @@ wheels = [ [[package]] name = "librt" -version = "0.7.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/84/2cfb1f3b9b60bab52e16a220c931223fc8e963d0d7bb9132bef012aafc3f/librt-0.7.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4836c5645f40fbdc275e5670819bde5ab5f2e882290d304e3c6ddab1576a6d0", size = 54709, upload-time = "2026-01-01T23:50:48.326Z" }, - { url = "https://files.pythonhosted.org/packages/19/a1/3127b277e9d3784a8040a54e8396d9ae5c64d6684dc6db4b4089b0eedcfb/librt-0.7.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae8aec43117a645a31e5f60e9e3a0797492e747823b9bda6972d521b436b4e8", size = 56658, upload-time = "2026-01-01T23:50:49.74Z" }, - { url = "https://files.pythonhosted.org/packages/3a/e9/b91b093a5c42eb218120445f3fef82e0b977fa2225f4d6fc133d25cdf86a/librt-0.7.7-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:aea05f701ccd2a76b34f0daf47ca5068176ff553510b614770c90d76ac88df06", size = 161026, upload-time = "2026-01-01T23:50:50.853Z" }, - { url = "https://files.pythonhosted.org/packages/c7/cb/1ded77d5976a79d7057af4a010d577ce4f473ff280984e68f4974a3281e5/librt-0.7.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b16ccaeff0ed4355dfb76fe1ea7a5d6d03b5ad27f295f77ee0557bc20a72495", size = 169529, upload-time = "2026-01-01T23:50:52.24Z" }, - { url = "https://files.pythonhosted.org/packages/da/6e/6ca5bdaa701e15f05000ac1a4c5d1475c422d3484bd3d1ca9e8c2f5be167/librt-0.7.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48c7e150c095d5e3cea7452347ba26094be905d6099d24f9319a8b475fcd3e0", size = 183271, upload-time = "2026-01-01T23:50:55.287Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2d/55c0e38073997b4bbb5ddff25b6d1bbba8c2f76f50afe5bb9c844b702f34/librt-0.7.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dcee2f921a8632636d1c37f1bbdb8841d15666d119aa61e5399c5268e7ce02e", size = 179039, upload-time = "2026-01-01T23:50:56.807Z" }, - { url = "https://files.pythonhosted.org/packages/33/4e/3662a41ae8bb81b226f3968426293517b271d34d4e9fd4b59fc511f1ae40/librt-0.7.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14ef0f4ac3728ffd85bfc58e2f2f48fb4ef4fa871876f13a73a7381d10a9f77c", size = 173505, upload-time = "2026-01-01T23:50:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5d/cf768deb8bdcbac5f8c21fcb32dd483d038d88c529fd351bbe50590b945d/librt-0.7.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4ab69fa37f8090f2d971a5d2bc606c7401170dbdae083c393d6cbf439cb45b8", size = 193570, upload-time = "2026-01-01T23:50:59.546Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ea/ee70effd13f1d651976d83a2812391f6203971740705e3c0900db75d4bce/librt-0.7.7-cp310-cp310-win32.whl", hash = "sha256:4bf3cc46d553693382d2abf5f5bd493d71bb0f50a7c0beab18aa13a5545c8900", size = 42600, upload-time = "2026-01-01T23:51:00.694Z" }, - { url = "https://files.pythonhosted.org/packages/f0/eb/dc098730f281cba76c279b71783f5de2edcba3b880c1ab84a093ef826062/librt-0.7.7-cp310-cp310-win_amd64.whl", hash = "sha256:f0c8fe5aeadd8a0e5b0598f8a6ee3533135ca50fd3f20f130f9d72baf5c6ac58", size = 48977, upload-time = "2026-01-01T23:51:01.726Z" }, - { url = "https://files.pythonhosted.org/packages/f0/56/30b5c342518005546df78841cb0820ae85a17e7d07d521c10ef367306d0d/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709, upload-time = "2026-01-01T23:51:02.774Z" }, - { url = "https://files.pythonhosted.org/packages/72/78/9f120e3920b22504d4f3835e28b55acc2cc47c9586d2e1b6ba04c3c1bf01/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663, upload-time = "2026-01-01T23:51:03.838Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ea/7d7a1ee7dfc1151836028eba25629afcf45b56bbc721293e41aa2e9b8934/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705, upload-time = "2026-01-01T23:51:04.917Z" }, - { url = "https://files.pythonhosted.org/packages/45/a5/952bc840ac8917fbcefd6bc5f51ad02b89721729814f3e2bfcc1337a76d6/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029, upload-time = "2026-01-01T23:51:06.09Z" }, - { url = "https://files.pythonhosted.org/packages/fa/bf/c017ff7da82dc9192cf40d5e802a48a25d00e7639b6465cfdcee5893a22c/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704, upload-time = "2026-01-01T23:51:07.549Z" }, - { url = "https://files.pythonhosted.org/packages/77/ec/72f3dd39d2cdfd6402ab10836dc9cbf854d145226062a185b419c4f1624a/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719, upload-time = "2026-01-01T23:51:09.062Z" }, - { url = "https://files.pythonhosted.org/packages/78/86/06e7a1a81b246f3313bf515dd9613a1c81583e6fd7843a9f4d625c4e926d/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537, upload-time = "2026-01-01T23:51:10.611Z" }, - { url = "https://files.pythonhosted.org/packages/83/08/f9fb2edc9c7a76e95b2924ce81d545673f5b034e8c5dd92159d1c7dae0c6/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238, upload-time = "2026-01-01T23:51:11.762Z" }, - { url = "https://files.pythonhosted.org/packages/ba/56/ea2d2489d3ea1f47b301120e03a099e22de7b32c93df9a211e6ff4f9bf38/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939, upload-time = "2026-01-01T23:51:13.192Z" }, - { url = "https://files.pythonhosted.org/packages/58/7b/c288f417e42ba2a037f1c0753219e277b33090ed4f72f292fb6fe175db4c/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240, upload-time = "2026-01-01T23:51:14.264Z" }, - { url = "https://files.pythonhosted.org/packages/7c/24/738eb33a6c1516fdb2dfd2a35db6e5300f7616679b573585be0409bc6890/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613, upload-time = "2026-01-01T23:51:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, - { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, - { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, - { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, - { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, - { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, - { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, - { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, - { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, - { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, - { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, - { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, - { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, - { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, - { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, - { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, - { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, - { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, - { url = "https://files.pythonhosted.org/packages/69/9c/228a5c1224bd23809a635490a162e9cbdc68d99f0eeb4a696f07886b8206/librt-0.7.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:23daa1ab0512bafdd677eb1bfc9611d8ffbe2e328895671e64cb34166bc1b8c8", size = 55188, upload-time = "2026-01-01T23:51:43.14Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c2/0e7c6067e2b32a156308205e5728f4ed6478c501947e9142f525afbc6bd2/librt-0.7.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:558a9e5a6f3cc1e20b3168fb1dc802d0d8fa40731f6e9932dcc52bbcfbd37111", size = 56895, upload-time = "2026-01-01T23:51:44.534Z" }, - { url = "https://files.pythonhosted.org/packages/0e/77/de50ff70c80855eb79d1d74035ef06f664dd073fb7fb9d9fb4429651b8eb/librt-0.7.7-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2567cb48dc03e5b246927ab35cbb343376e24501260a9b5e30b8e255dca0d1d2", size = 163724, upload-time = "2026-01-01T23:51:45.571Z" }, - { url = "https://files.pythonhosted.org/packages/6e/19/f8e4bf537899bdef9e0bb9f0e4b18912c2d0f858ad02091b6019864c9a6d/librt-0.7.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6066c638cdf85ff92fc6f932d2d73c93a0e03492cdfa8778e6d58c489a3d7259", size = 172470, upload-time = "2026-01-01T23:51:46.823Z" }, - { url = "https://files.pythonhosted.org/packages/42/4c/dcc575b69d99076768e8dd6141d9aecd4234cba7f0e09217937f52edb6ed/librt-0.7.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a609849aca463074c17de9cda173c276eb8fee9e441053529e7b9e249dc8b8ee", size = 186806, upload-time = "2026-01-01T23:51:48.009Z" }, - { url = "https://files.pythonhosted.org/packages/fe/f8/4094a2b7816c88de81239a83ede6e87f1138477d7ee956c30f136009eb29/librt-0.7.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:add4e0a000858fe9bb39ed55f31085506a5c38363e6eb4a1e5943a10c2bfc3d1", size = 181809, upload-time = "2026-01-01T23:51:49.35Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ac/821b7c0ab1b5a6cd9aee7ace8309c91545a2607185101827f79122219a7e/librt-0.7.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a3bfe73a32bd0bdb9a87d586b05a23c0a1729205d79df66dee65bb2e40d671ba", size = 175597, upload-time = "2026-01-01T23:51:50.636Z" }, - { url = "https://files.pythonhosted.org/packages/71/f9/27f6bfbcc764805864c04211c6ed636fe1d58f57a7b68d1f4ae5ed74e0e0/librt-0.7.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0ecce0544d3db91a40f8b57ae26928c02130a997b540f908cefd4d279d6c5848", size = 196506, upload-time = "2026-01-01T23:51:52.535Z" }, - { url = "https://files.pythonhosted.org/packages/46/ba/c9b9c6fc931dd7ea856c573174ccaf48714905b1a7499904db2552e3bbaf/librt-0.7.7-cp314-cp314-win32.whl", hash = "sha256:8f7a74cf3a80f0c3b0ec75b0c650b2f0a894a2cec57ef75f6f72c1e82cdac61d", size = 39747, upload-time = "2026-01-01T23:51:53.683Z" }, - { url = "https://files.pythonhosted.org/packages/c5/69/cd1269337c4cde3ee70176ee611ab0058aa42fc8ce5c9dce55f48facfcd8/librt-0.7.7-cp314-cp314-win_amd64.whl", hash = "sha256:3d1fe2e8df3268dd6734dba33ededae72ad5c3a859b9577bc00b715759c5aaab", size = 45971, upload-time = "2026-01-01T23:51:54.697Z" }, - { url = "https://files.pythonhosted.org/packages/79/fd/e0844794423f5583108c5991313c15e2b400995f44f6ec6871f8aaf8243c/librt-0.7.7-cp314-cp314-win_arm64.whl", hash = "sha256:2987cf827011907d3dfd109f1be0d61e173d68b1270107bb0e89f2fca7f2ed6b", size = 39075, upload-time = "2026-01-01T23:51:55.726Z" }, - { url = "https://files.pythonhosted.org/packages/42/02/211fd8f7c381e7b2a11d0fdfcd410f409e89967be2e705983f7c6342209a/librt-0.7.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e92c8de62b40bfce91d5e12c6e8b15434da268979b1af1a6589463549d491e6", size = 57368, upload-time = "2026-01-01T23:51:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/aca257affae73ece26041ae76032153266d110453173f67d7603058e708c/librt-0.7.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f683dcd49e2494a7535e30f779aa1ad6e3732a019d80abe1309ea91ccd3230e3", size = 59238, upload-time = "2026-01-01T23:51:58.066Z" }, - { url = "https://files.pythonhosted.org/packages/96/47/7383a507d8e0c11c78ca34c9d36eab9000db5989d446a2f05dc40e76c64f/librt-0.7.7-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b15e5d17812d4d629ff576699954f74e2cc24a02a4fc401882dd94f81daba45", size = 183870, upload-time = "2026-01-01T23:51:59.204Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b8/50f3d8eec8efdaf79443963624175c92cec0ba84827a66b7fcfa78598e51/librt-0.7.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c084841b879c4d9b9fa34e5d5263994f21aea7fd9c6add29194dbb41a6210536", size = 194608, upload-time = "2026-01-01T23:52:00.419Z" }, - { url = "https://files.pythonhosted.org/packages/23/d9/1b6520793aadb59d891e3b98ee057a75de7f737e4a8b4b37fdbecb10d60f/librt-0.7.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c8fb9966f84737115513fecbaf257f9553d067a7dd45a69c2c7e5339e6a8dc", size = 206776, upload-time = "2026-01-01T23:52:01.705Z" }, - { url = "https://files.pythonhosted.org/packages/ff/db/331edc3bba929d2756fa335bfcf736f36eff4efcb4f2600b545a35c2ae58/librt-0.7.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5fb1ecb2c35362eab2dbd354fd1efa5a8440d3e73a68be11921042a0edc0ff", size = 203206, upload-time = "2026-01-01T23:52:03.315Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e1/6af79ec77204e85f6f2294fc171a30a91bb0e35d78493532ed680f5d98be/librt-0.7.7-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d1454899909d63cc9199a89fcc4f81bdd9004aef577d4ffc022e600c412d57f3", size = 196697, upload-time = "2026-01-01T23:52:04.857Z" }, - { url = "https://files.pythonhosted.org/packages/f3/46/de55ecce4b2796d6d243295c221082ca3a944dc2fb3a52dcc8660ce7727d/librt-0.7.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7ef28f2e7a016b29792fe0a2dd04dec75725b32a1264e390c366103f834a9c3a", size = 217193, upload-time = "2026-01-01T23:52:06.159Z" }, - { url = "https://files.pythonhosted.org/packages/41/61/33063e271949787a2f8dd33c5260357e3d512a114fc82ca7890b65a76e2d/librt-0.7.7-cp314-cp314t-win32.whl", hash = "sha256:5e419e0db70991b6ba037b70c1d5bbe92b20ddf82f31ad01d77a347ed9781398", size = 40277, upload-time = "2026-01-01T23:52:07.625Z" }, - { url = "https://files.pythonhosted.org/packages/06/21/1abd972349f83a696ea73159ac964e63e2d14086fdd9bc7ca878c25fced4/librt-0.7.7-cp314-cp314t-win_amd64.whl", hash = "sha256:d6b7d93657332c817b8d674ef6bf1ab7796b4f7ce05e420fd45bd258a72ac804", size = 46765, upload-time = "2026-01-01T23:52:08.647Z" }, - { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, +version = "0.7.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" }, + { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" }, + { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" }, + { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" }, + { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" }, + { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" }, + { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" }, + { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" }, + { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" }, + { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" }, + { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" }, + { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" }, + { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" }, + { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" }, + { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" }, + { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" }, + { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" }, + { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" }, + { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" }, + { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" }, + { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" }, + { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, ] [[package]] name = "markdown-it-py" version = "3.0.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ - { name = "mdurl" }, + { name = "mdurl", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -1138,7 +1175,8 @@ name = "mdit-py-plugins" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } wheels = [ @@ -1225,20 +1263,54 @@ wheels = [ name = "myst-parser" version = "4.0.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "jinja2", marker = "python_full_version < '3.11'" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "mdit-py-plugins", marker = "python_full_version < '3.11'" }, + { name = "pyyaml", marker = "python_full_version < '3.11'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579, upload-time = "2025-02-12T10:53:02.078Z" }, ] +[[package]] +name = "myst-parser" +version = "5.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", + "python_full_version == '3.11.*'", +] +dependencies = [ + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "jinja2", marker = "python_full_version >= '3.11'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, + { name = "pyyaml", marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/fa/7b45eef11b7971f0beb29d27b7bfe0d747d063aa29e170d9edd004733c8a/myst_parser-5.0.0.tar.gz", hash = "sha256:f6f231452c56e8baa662cc352c548158f6a16fcbd6e3800fc594978002b94f3a", size = 98535, upload-time = "2026-01-15T09:08:18.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/ac/686789b9145413f1a61878c407210e41bfdb097976864e0913078b24098c/myst_parser-5.0.0-py3-none-any.whl", hash = "sha256:ab31e516024918296e169139072b81592336f2fef55b8986aa31c9f04b5f7211", size = 84533, upload-time = "2026-01-15T09:08:16.788Z" }, +] + +[[package]] +name = "narwhals" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/6d/b57c64e5038a8cf071bce391bb11551657a74558877ac961e7fa905ece27/narwhals-2.15.0.tar.gz", hash = "sha256:a9585975b99d95084268445a1fdd881311fa26ef1caa18020d959d5b2ff9a965", size = 603479, upload-time = "2026-01-06T08:10:13.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/2e/cf2ffeb386ac3763526151163ad7da9f1b586aac96d2b4f7de1eaebf0c61/narwhals-2.15.0-py3-none-any.whl", hash = "sha256:cbfe21ca19d260d9fd67f995ec75c44592d1f106933b03ddd375df7ac841f9d6", size = 432856, upload-time = "2026-01-06T08:10:11.511Z" }, +] + [[package]] name = "nbclient" version = "0.10.4" @@ -1391,86 +1463,86 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.0" +version = "2.4.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version == '3.11.*'", ] -sdist = { url = "https://files.pythonhosted.org/packages/a4/7a/6a3d14e205d292b738db449d0de649b373a59edb0d0b4493821d0a3e8718/numpy-2.4.0.tar.gz", hash = "sha256:6e504f7b16118198f138ef31ba24d985b124c2c469fe8467007cf30fd992f934", size = 20685720, upload-time = "2025-12-20T16:18:19.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/7e/7bae7cbcc2f8132271967aa03e03954fc1e48aa1f3bf32b29ca95fbef352/numpy-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:316b2f2584682318539f0bcaca5a496ce9ca78c88066579ebd11fd06f8e4741e", size = 16940166, upload-time = "2025-12-20T16:15:43.434Z" }, - { url = "https://files.pythonhosted.org/packages/0f/27/6c13f5b46776d6246ec884ac5817452672156a506d08a1f2abb39961930a/numpy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2718c1de8504121714234b6f8241d0019450353276c88b9453c9c3d92e101db", size = 12641781, upload-time = "2025-12-20T16:15:45.701Z" }, - { url = "https://files.pythonhosted.org/packages/14/1c/83b4998d4860d15283241d9e5215f28b40ac31f497c04b12fa7f428ff370/numpy-2.4.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:21555da4ec4a0c942520ead42c3b0dc9477441e085c42b0fbdd6a084869a6f6b", size = 5470247, upload-time = "2025-12-20T16:15:47.943Z" }, - { url = "https://files.pythonhosted.org/packages/54/08/cbce72c835d937795571b0464b52069f869c9e78b0c076d416c5269d2718/numpy-2.4.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:413aa561266a4be2d06cd2b9665e89d9f54c543f418773076a76adcf2af08bc7", size = 6799807, upload-time = "2025-12-20T16:15:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/ff/be/2e647961cd8c980591d75cdcd9e8f647d69fbe05e2a25613dc0a2ea5fb1a/numpy-2.4.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0feafc9e03128074689183031181fac0897ff169692d8492066e949041096548", size = 14701992, upload-time = "2025-12-20T16:15:51.615Z" }, - { url = "https://files.pythonhosted.org/packages/a2/fb/e1652fb8b6fd91ce6ed429143fe2e01ce714711e03e5b762615e7b36172c/numpy-2.4.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8fdfed3deaf1928fb7667d96e0567cdf58c2b370ea2ee7e586aa383ec2cb346", size = 16646871, upload-time = "2025-12-20T16:15:54.129Z" }, - { url = "https://files.pythonhosted.org/packages/62/23/d841207e63c4322842f7cd042ae981cffe715c73376dcad8235fb31debf1/numpy-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e06a922a469cae9a57100864caf4f8a97a1026513793969f8ba5b63137a35d25", size = 16487190, upload-time = "2025-12-20T16:15:56.147Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/6a842c8421ebfdec0a230e65f61e0dabda6edbef443d999d79b87c273965/numpy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:927ccf5cd17c48f801f4ed43a7e5673a2724bd2171460be3e3894e6e332ef83a", size = 18580762, upload-time = "2025-12-20T16:15:58.524Z" }, - { url = "https://files.pythonhosted.org/packages/0a/d1/c79e0046641186f2134dde05e6181825b911f8bdcef31b19ddd16e232847/numpy-2.4.0-cp311-cp311-win32.whl", hash = "sha256:882567b7ae57c1b1a0250208cc21a7976d8cbcc49d5a322e607e6f09c9e0bd53", size = 6233359, upload-time = "2025-12-20T16:16:00.938Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f0/74965001d231f28184d6305b8cdc1b6fcd4bf23033f6cb039cfe76c9fca7/numpy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:8b986403023c8f3bf8f487c2e6186afda156174d31c175f747d8934dfddf3479", size = 12601132, upload-time = "2025-12-20T16:16:02.484Z" }, - { url = "https://files.pythonhosted.org/packages/65/32/55408d0f46dfebce38017f5bd931affa7256ad6beac1a92a012e1fbc67a7/numpy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:3f3096405acc48887458bbf9f6814d43785ac7ba2a57ea6442b581dedbc60ce6", size = 10573977, upload-time = "2025-12-20T16:16:04.77Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ff/f6400ffec95de41c74b8e73df32e3fff1830633193a7b1e409be7fb1bb8c/numpy-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8b6bb8369abefb8bd1801b054ad50e02b3275c8614dc6e5b0373c305291037", size = 16653117, upload-time = "2025-12-20T16:16:06.709Z" }, - { url = "https://files.pythonhosted.org/packages/fd/28/6c23e97450035072e8d830a3c411bf1abd1f42c611ff9d29e3d8f55c6252/numpy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e284ca13d5a8367e43734148622caf0b261b275673823593e3e3634a6490f83", size = 12369711, upload-time = "2025-12-20T16:16:08.758Z" }, - { url = "https://files.pythonhosted.org/packages/bc/af/acbef97b630ab1bb45e6a7d01d1452e4251aa88ce680ac36e56c272120ec/numpy-2.4.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:49ff32b09f5aa0cd30a20c2b39db3e669c845589f2b7fc910365210887e39344", size = 5198355, upload-time = "2025-12-20T16:16:10.902Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c8/4e0d436b66b826f2e53330adaa6311f5cac9871a5b5c31ad773b27f25a74/numpy-2.4.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:36cbfb13c152b1c7c184ddac43765db8ad672567e7bafff2cc755a09917ed2e6", size = 6545298, upload-time = "2025-12-20T16:16:12.607Z" }, - { url = "https://files.pythonhosted.org/packages/ef/27/e1f5d144ab54eac34875e79037011d511ac57b21b220063310cb96c80fbc/numpy-2.4.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35ddc8f4914466e6fc954c76527aa91aa763682a4f6d73249ef20b418fe6effb", size = 14398387, upload-time = "2025-12-20T16:16:14.257Z" }, - { url = "https://files.pythonhosted.org/packages/67/64/4cb909dd5ab09a9a5d086eff9586e69e827b88a5585517386879474f4cf7/numpy-2.4.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc578891de1db95b2a35001b695451767b580bb45753717498213c5ff3c41d63", size = 16363091, upload-time = "2025-12-20T16:16:17.32Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9c/8efe24577523ec6809261859737cf117b0eb6fdb655abdfdc81b2e468ce4/numpy-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98e81648e0b36e325ab67e46b5400a7a6d4a22b8a7c8e8bbfe20e7db7906bf95", size = 16176394, upload-time = "2025-12-20T16:16:19.524Z" }, - { url = "https://files.pythonhosted.org/packages/61/f0/1687441ece7b47a62e45a1f82015352c240765c707928edd8aef875d5951/numpy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d57b5046c120561ba8fa8e4030fbb8b822f3063910fa901ffadf16e2b7128ad6", size = 18287378, upload-time = "2025-12-20T16:16:22.866Z" }, - { url = "https://files.pythonhosted.org/packages/d3/6f/f868765d44e6fc466467ed810ba9d8d6db1add7d4a748abfa2a4c99a3194/numpy-2.4.0-cp312-cp312-win32.whl", hash = "sha256:92190db305a6f48734d3982f2c60fa30d6b5ee9bff10f2887b930d7b40119f4c", size = 5955432, upload-time = "2025-12-20T16:16:25.06Z" }, - { url = "https://files.pythonhosted.org/packages/d4/b5/94c1e79fcbab38d1ca15e13777477b2914dd2d559b410f96949d6637b085/numpy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:680060061adb2d74ce352628cb798cfdec399068aa7f07ba9fb818b2b3305f98", size = 12306201, upload-time = "2025-12-20T16:16:26.979Z" }, - { url = "https://files.pythonhosted.org/packages/70/09/c39dadf0b13bb0768cd29d6a3aaff1fb7c6905ac40e9aaeca26b1c086e06/numpy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:39699233bc72dd482da1415dcb06076e32f60eddc796a796c5fb6c5efce94667", size = 10308234, upload-time = "2025-12-20T16:16:29.417Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0d/853fd96372eda07c824d24adf02e8bc92bb3731b43a9b2a39161c3667cc4/numpy-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a152d86a3ae00ba5f47b3acf3b827509fd0b6cb7d3259665e63dafbad22a75ea", size = 16649088, upload-time = "2025-12-20T16:16:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/e3/37/cc636f1f2a9f585434e20a3e6e63422f70bfe4f7f6698e941db52ea1ac9a/numpy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:39b19251dec4de8ff8496cd0806cbe27bf0684f765abb1f4809554de93785f2d", size = 12364065, upload-time = "2025-12-20T16:16:33.491Z" }, - { url = "https://files.pythonhosted.org/packages/ed/69/0b78f37ca3690969beee54103ce5f6021709134e8020767e93ba691a72f1/numpy-2.4.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:009bd0ea12d3c784b6639a8457537016ce5172109e585338e11334f6a7bb88ee", size = 5192640, upload-time = "2025-12-20T16:16:35.636Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2a/08569f8252abf590294dbb09a430543ec8f8cc710383abfb3e75cc73aeda/numpy-2.4.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5fe44e277225fd3dff6882d86d3d447205d43532c3627313d17e754fb3905a0e", size = 6541556, upload-time = "2025-12-20T16:16:37.276Z" }, - { url = "https://files.pythonhosted.org/packages/93/e9/a949885a4e177493d61519377952186b6cbfdf1d6002764c664ba28349b5/numpy-2.4.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c4493eda9069851058fa0d9e39dbf6286be690066509305e52912714dbb2", size = 14396562, upload-time = "2025-12-20T16:16:38.953Z" }, - { url = "https://files.pythonhosted.org/packages/99/98/9d4ad53b0e9ef901c2ef1d550d2136f5ac42d3fd2988390a6def32e23e48/numpy-2.4.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cfa5f29a695cb7438965e6c3e8d06e0416060cf0d709c1b1c1653a939bf5c2a", size = 16351719, upload-time = "2025-12-20T16:16:41.503Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/5f3711a38341d6e8dd619f6353251a0cdd07f3d6d101a8fd46f4ef87f895/numpy-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba0cb30acd3ef11c94dc27fbfba68940652492bc107075e7ffe23057f9425681", size = 16176053, upload-time = "2025-12-20T16:16:44.552Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5b/2a3753dc43916501b4183532e7ace862e13211042bceafa253afb5c71272/numpy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60e8c196cd82cbbd4f130b5290007e13e6de3eca79f0d4d38014769d96a7c475", size = 18277859, upload-time = "2025-12-20T16:16:47.174Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c5/a18bcdd07a941db3076ef489d036ab16d2bfc2eae0cf27e5a26e29189434/numpy-2.4.0-cp313-cp313-win32.whl", hash = "sha256:5f48cb3e88fbc294dc90e215d86fbaf1c852c63dbdb6c3a3e63f45c4b57f7344", size = 5953849, upload-time = "2025-12-20T16:16:49.554Z" }, - { url = "https://files.pythonhosted.org/packages/4f/f1/719010ff8061da6e8a26e1980cf090412d4f5f8060b31f0c45d77dd67a01/numpy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:a899699294f28f7be8992853c0c60741f16ff199205e2e6cdca155762cbaa59d", size = 12302840, upload-time = "2025-12-20T16:16:51.227Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/b3d259083ed8b4d335270c76966cb6cf14a5d1b69e1a608994ac57a659e6/numpy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:9198f447e1dc5647d07c9a6bbe2063cc0132728cc7175b39dbc796da5b54920d", size = 10308509, upload-time = "2025-12-20T16:16:53.313Z" }, - { url = "https://files.pythonhosted.org/packages/31/01/95edcffd1bb6c0633df4e808130545c4f07383ab629ac7e316fb44fff677/numpy-2.4.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74623f2ab5cc3f7c886add4f735d1031a1d2be4a4ae63c0546cfd74e7a31ddf6", size = 12491815, upload-time = "2025-12-20T16:16:55.496Z" }, - { url = "https://files.pythonhosted.org/packages/59/ea/5644b8baa92cc1c7163b4b4458c8679852733fa74ca49c942cfa82ded4e0/numpy-2.4.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0804a8e4ab070d1d35496e65ffd3cf8114c136a2b81f61dfab0de4b218aacfd5", size = 5320321, upload-time = "2025-12-20T16:16:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/4e/e10938106d70bc21319bd6a86ae726da37edc802ce35a3a71ecdf1fdfe7f/numpy-2.4.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:02a2038eb27f9443a8b266a66911e926566b5a6ffd1a689b588f7f35b81e7dc3", size = 6641635, upload-time = "2025-12-20T16:16:59.379Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8d/a8828e3eaf5c0b4ab116924df82f24ce3416fa38d0674d8f708ddc6c8aac/numpy-2.4.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1889b3a3f47a7b5bee16bc25a2145bd7cb91897f815ce3499db64c7458b6d91d", size = 14456053, upload-time = "2025-12-20T16:17:01.768Z" }, - { url = "https://files.pythonhosted.org/packages/68/a1/17d97609d87d4520aa5ae2dcfb32305654550ac6a35effb946d303e594ce/numpy-2.4.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85eef4cb5625c47ee6425c58a3502555e10f45ee973da878ac8248ad58c136f3", size = 16401702, upload-time = "2025-12-20T16:17:04.235Z" }, - { url = "https://files.pythonhosted.org/packages/18/32/0f13c1b2d22bea1118356b8b963195446f3af124ed7a5adfa8fdecb1b6ca/numpy-2.4.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6dc8b7e2f4eb184b37655195f421836cfae6f58197b67e3ffc501f1333d993fa", size = 16242493, upload-time = "2025-12-20T16:17:06.856Z" }, - { url = "https://files.pythonhosted.org/packages/ae/23/48f21e3d309fbc137c068a1475358cbd3a901b3987dcfc97a029ab3068e2/numpy-2.4.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:44aba2f0cafd287871a495fb3163408b0bd25bbce135c6f621534a07f4f7875c", size = 18324222, upload-time = "2025-12-20T16:17:09.392Z" }, - { url = "https://files.pythonhosted.org/packages/ac/52/41f3d71296a3dcaa4f456aaa3c6fc8e745b43d0552b6bde56571bb4b4a0f/numpy-2.4.0-cp313-cp313t-win32.whl", hash = "sha256:20c115517513831860c573996e395707aa9fb691eb179200125c250e895fcd93", size = 6076216, upload-time = "2025-12-20T16:17:11.437Z" }, - { url = "https://files.pythonhosted.org/packages/35/ff/46fbfe60ab0710d2a2b16995f708750307d30eccbb4c38371ea9e986866e/numpy-2.4.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b48e35f4ab6f6a7597c46e301126ceba4c44cd3280e3750f85db48b082624fa4", size = 12444263, upload-time = "2025-12-20T16:17:13.182Z" }, - { url = "https://files.pythonhosted.org/packages/a3/e3/9189ab319c01d2ed556c932ccf55064c5d75bb5850d1df7a482ce0badead/numpy-2.4.0-cp313-cp313t-win_arm64.whl", hash = "sha256:4d1cfce39e511069b11e67cd0bd78ceff31443b7c9e5c04db73c7a19f572967c", size = 10378265, upload-time = "2025-12-20T16:17:15.211Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ed/52eac27de39d5e5a6c9aadabe672bc06f55e24a3d9010cd1183948055d76/numpy-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c95eb6db2884917d86cde0b4d4cf31adf485c8ec36bf8696dd66fa70de96f36b", size = 16647476, upload-time = "2025-12-20T16:17:17.671Z" }, - { url = "https://files.pythonhosted.org/packages/77/c0/990ce1b7fcd4e09aeaa574e2a0a839589e4b08b2ca68070f1acb1fea6736/numpy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:65167da969cd1ec3a1df31cb221ca3a19a8aaa25370ecb17d428415e93c1935e", size = 12374563, upload-time = "2025-12-20T16:17:20.216Z" }, - { url = "https://files.pythonhosted.org/packages/37/7c/8c5e389c6ae8f5fd2277a988600d79e9625db3fff011a2d87ac80b881a4c/numpy-2.4.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3de19cfecd1465d0dcf8a5b5ea8b3155b42ed0b639dba4b71e323d74f2a3be5e", size = 5203107, upload-time = "2025-12-20T16:17:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/e6/94/ca5b3bd6a8a70a5eec9a0b8dd7f980c1eff4b8a54970a9a7fef248ef564f/numpy-2.4.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6c05483c3136ac4c91b4e81903cb53a8707d316f488124d0398499a4f8e8ef51", size = 6538067, upload-time = "2025-12-20T16:17:24.001Z" }, - { url = "https://files.pythonhosted.org/packages/79/43/993eb7bb5be6761dde2b3a3a594d689cec83398e3f58f4758010f3b85727/numpy-2.4.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36667db4d6c1cea79c8930ab72fadfb4060feb4bfe724141cd4bd064d2e5f8ce", size = 14411926, upload-time = "2025-12-20T16:17:25.822Z" }, - { url = "https://files.pythonhosted.org/packages/03/75/d4c43b61de473912496317a854dac54f1efec3eeb158438da6884b70bb90/numpy-2.4.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a818668b674047fd88c4cddada7ab8f1c298812783e8328e956b78dc4807f9f", size = 16354295, upload-time = "2025-12-20T16:17:28.308Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0a/b54615b47ee8736a6461a4bb6749128dd3435c5a759d5663f11f0e9af4ac/numpy-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ee32359fb7543b7b7bd0b2f46294db27e29e7bbdf70541e81b190836cd83ded", size = 16190242, upload-time = "2025-12-20T16:17:30.993Z" }, - { url = "https://files.pythonhosted.org/packages/98/ce/ea207769aacad6246525ec6c6bbd66a2bf56c72443dc10e2f90feed29290/numpy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e493962256a38f58283de033d8af176c5c91c084ea30f15834f7545451c42059", size = 18280875, upload-time = "2025-12-20T16:17:33.327Z" }, - { url = "https://files.pythonhosted.org/packages/17/ef/ec409437aa962ea372ed601c519a2b141701683ff028f894b7466f0ab42b/numpy-2.4.0-cp314-cp314-win32.whl", hash = "sha256:6bbaebf0d11567fa8926215ae731e1d58e6ec28a8a25235b8a47405d301332db", size = 6002530, upload-time = "2025-12-20T16:17:35.729Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4a/5cb94c787a3ed1ac65e1271b968686521169a7b3ec0b6544bb3ca32960b0/numpy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d857f55e7fdf7c38ab96c4558c95b97d1c685be6b05c249f5fdafcbd6f9899e", size = 12435890, upload-time = "2025-12-20T16:17:37.599Z" }, - { url = "https://files.pythonhosted.org/packages/48/a0/04b89db963af9de1104975e2544f30de89adbf75b9e75f7dd2599be12c79/numpy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:bb50ce5fb202a26fd5404620e7ef820ad1ab3558b444cb0b55beb7ef66cd2d63", size = 10591892, upload-time = "2025-12-20T16:17:39.649Z" }, - { url = "https://files.pythonhosted.org/packages/53/e5/d74b5ccf6712c06c7a545025a6a71bfa03bdc7e0568b405b0d655232fd92/numpy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:355354388cba60f2132df297e2d53053d4063f79077b67b481d21276d61fc4df", size = 12494312, upload-time = "2025-12-20T16:17:41.714Z" }, - { url = "https://files.pythonhosted.org/packages/c2/08/3ca9cc2ddf54dfee7ae9a6479c071092a228c68aef08252aa08dac2af002/numpy-2.4.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:1d8f9fde5f6dc1b6fc34df8162f3b3079365468703fee7f31d4e0cc8c63baed9", size = 5322862, upload-time = "2025-12-20T16:17:44.145Z" }, - { url = "https://files.pythonhosted.org/packages/87/74/0bb63a68394c0c1e52670cfff2e309afa41edbe11b3327d9af29e4383f34/numpy-2.4.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e0434aa22c821f44eeb4c650b81c7fbdd8c0122c6c4b5a576a76d5a35625ecd9", size = 6644986, upload-time = "2025-12-20T16:17:46.203Z" }, - { url = "https://files.pythonhosted.org/packages/06/8f/9264d9bdbcf8236af2823623fe2f3981d740fc3461e2787e231d97c38c28/numpy-2.4.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40483b2f2d3ba7aad426443767ff5632ec3156ef09742b96913787d13c336471", size = 14457958, upload-time = "2025-12-20T16:17:48.017Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d9/f9a69ae564bbc7236a35aa883319364ef5fd41f72aa320cc1cbe66148fe2/numpy-2.4.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6a7664ddd9746e20b7325351fe1a8408d0a2bf9c63b5e898290ddc8f09544", size = 16398394, upload-time = "2025-12-20T16:17:50.409Z" }, - { url = "https://files.pythonhosted.org/packages/34/c7/39241501408dde7f885d241a98caba5421061a2c6d2b2197ac5e3aa842d8/numpy-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ecb0019d44f4cdb50b676c5d0cb4b1eae8e15d1ed3d3e6639f986fc92b2ec52c", size = 16241044, upload-time = "2025-12-20T16:17:52.661Z" }, - { url = "https://files.pythonhosted.org/packages/7c/95/cae7effd90e065a95e59fe710eeee05d7328ed169776dfdd9f789e032125/numpy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d0ffd9e2e4441c96a9c91ec1783285d80bf835b677853fc2770a89d50c1e48ac", size = 18321772, upload-time = "2025-12-20T16:17:54.947Z" }, - { url = "https://files.pythonhosted.org/packages/96/df/3c6c279accd2bfb968a76298e5b276310bd55d243df4fa8ac5816d79347d/numpy-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:77f0d13fa87036d7553bf81f0e1fe3ce68d14c9976c9851744e4d3e91127e95f", size = 6148320, upload-time = "2025-12-20T16:17:57.249Z" }, - { url = "https://files.pythonhosted.org/packages/92/8d/f23033cce252e7a75cae853d17f582e86534c46404dea1c8ee094a9d6d84/numpy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b1f5b45829ac1848893f0ddf5cb326110604d6df96cdc255b0bf9edd154104d4", size = 12623460, upload-time = "2025-12-20T16:17:58.963Z" }, - { url = "https://files.pythonhosted.org/packages/a4/4f/1f8475907d1a7c4ef9020edf7f39ea2422ec896849245f00688e4b268a71/numpy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:23a3e9d1a6f360267e8fbb38ba5db355a6a7e9be71d7fce7ab3125e88bb646c8", size = 10661799, upload-time = "2025-12-20T16:18:01.078Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ef/088e7c7342f300aaf3ee5f2c821c4b9996a1bef2aaf6a49cc8ab4883758e/numpy-2.4.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b54c83f1c0c0f1d748dca0af516062b8829d53d1f0c402be24b4257a9c48ada6", size = 16819003, upload-time = "2025-12-20T16:18:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ce/a53017b5443b4b84517182d463fc7bcc2adb4faa8b20813f8e5f5aeb5faa/numpy-2.4.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:aabb081ca0ec5d39591fc33018cd4b3f96e1a2dd6756282029986d00a785fba4", size = 12567105, upload-time = "2025-12-20T16:18:05.594Z" }, - { url = "https://files.pythonhosted.org/packages/77/58/5ff91b161f2ec650c88a626c3905d938c89aaadabd0431e6d9c1330c83e2/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:8eafe7c36c8430b7794edeab3087dec7bf31d634d92f2af9949434b9d1964cba", size = 5395590, upload-time = "2025-12-20T16:18:08.031Z" }, - { url = "https://files.pythonhosted.org/packages/1d/4e/f1a084106df8c2df8132fc437e56987308e0524836aa7733721c8429d4fe/numpy-2.4.0-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2f585f52b2baf07ff3356158d9268ea095e221371f1074fadea2f42544d58b4d", size = 6709947, upload-time = "2025-12-20T16:18:09.836Z" }, - { url = "https://files.pythonhosted.org/packages/63/09/3d8aeb809c0332c3f642da812ac2e3d74fc9252b3021f8c30c82e99e3f3d/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32ed06d0fe9cae27d8fb5f400c63ccee72370599c75e683a6358dd3a4fb50aaf", size = 14535119, upload-time = "2025-12-20T16:18:12.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7f/68f0fc43a2cbdc6bb239160c754d87c922f60fbaa0fa3cd3d312b8a7f5ee/numpy-2.4.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57c540ed8fb1f05cb997c6761cd56db72395b0d6985e90571ff660452ade4f98", size = 16475815, upload-time = "2025-12-20T16:18:14.433Z" }, - { url = "https://files.pythonhosted.org/packages/11/73/edeacba3167b1ca66d51b1a5a14697c2c40098b5ffa01811c67b1785a5ab/numpy-2.4.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a39fb973a726e63223287adc6dafe444ce75af952d711e400f3bf2b36ef55a7b", size = 12489376, upload-time = "2025-12-20T16:18:16.524Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, + { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, + { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" }, + { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" }, + { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" }, + { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" }, + { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" }, + { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" }, + { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" }, + { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" }, + { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" }, + { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" }, + { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" }, + { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" }, + { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" }, + { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, + { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, + { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, ] [[package]] @@ -1497,7 +1569,7 @@ version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "python-dateutil" }, { name = "pytz" }, { name = "tzdata" }, @@ -1573,11 +1645,11 @@ wheels = [ [[package]] name = "pathspec" -version = "0.12.1" +version = "1.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, ] [[package]] @@ -1610,6 +1682,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "polars" +version = "1.37.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/ae/dfebf31b9988c20998140b54d5b521f64ce08879f2c13d9b4d44d7c87e32/polars-1.37.1.tar.gz", hash = "sha256:0309e2a4633e712513401964b4d95452f124ceabf7aec6db50affb9ced4a274e", size = 715572, upload-time = "2026-01-12T23:27:03.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/75/ec73e38812bca7c2240aff481b9ddff20d1ad2f10dee4b3353f5eeaacdab/polars-1.37.1-py3-none-any.whl", hash = "sha256:377fed8939a2f1223c1563cfabdc7b4a3d6ff846efa1f2ddeb8644fafd9b1aff", size = 805749, upload-time = "2026-01-12T23:25:48.595Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.37.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/0b/addabe5e8d28a5a4c9887a08907be7ddc3fce892dc38f37d14b055438a57/polars_runtime_32-1.37.1.tar.gz", hash = "sha256:68779d4a691da20a5eb767d74165a8f80a2bdfbde4b54acf59af43f7fa028d8f", size = 2818945, upload-time = "2026-01-12T23:27:04.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a2/e828ea9f845796de02d923edb790e408ca0b560cd68dbd74bb99a1b3c461/polars_runtime_32-1.37.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0b8d4d73ea9977d3731927740e59d814647c5198bdbe359bcf6a8bfce2e79771", size = 43499912, upload-time = "2026-01-12T23:25:51.182Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/81b71b7aa9e3703ee6e4ef1f69a87e40f58ea7c99212bf49a95071e99c8c/polars_runtime_32-1.37.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c682bf83f5f352e5e02f5c16c652c48ca40442f07b236f30662b22217320ce76", size = 39695707, upload-time = "2026-01-12T23:25:54.289Z" }, + { url = "https://files.pythonhosted.org/packages/81/2e/20009d1fde7ee919e24040f5c87cb9d0e4f8e3f109b74ba06bc10c02459c/polars_runtime_32-1.37.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc82b5bbe70ca1a4b764eed1419f6336752d6ba9fc1245388d7f8b12438afa2c", size = 41467034, upload-time = "2026-01-12T23:25:56.925Z" }, + { url = "https://files.pythonhosted.org/packages/eb/21/9b55bea940524324625b1e8fd96233290303eb1bf2c23b54573487bbbc25/polars_runtime_32-1.37.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8362d11ac5193b994c7e9048ffe22ccfb976699cfbf6e128ce0302e06728894", size = 45142711, upload-time = "2026-01-12T23:26:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/c5f64461aeccdac6834a89f826d051ccd3b4ce204075e562c87a06ed2619/polars_runtime_32-1.37.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04f5d5a2f013dca7391b7d8e7672fa6d37573a87f1d45d3dd5f0d9b5565a4b0f", size = 41638564, upload-time = "2026-01-12T23:26:04.186Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/509d3cf6c45e764ccf856beaae26fc34352f16f10f94a7839b1042920a73/polars_runtime_32-1.37.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fbfde7c0ca8209eeaed546e4a32cca1319189aa61c5f0f9a2b4494262bd0c689", size = 44721136, upload-time = "2026-01-12T23:26:07.088Z" }, + { url = "https://files.pythonhosted.org/packages/af/d1/5c0a83a625f72beef59394bebc57d12637997632a4f9d3ab2ffc2cc62bbf/polars_runtime_32-1.37.1-cp310-abi3-win_amd64.whl", hash = "sha256:da3d3642ae944e18dd17109d2a3036cb94ce50e5495c5023c77b1599d4c861bc", size = 44948288, upload-time = "2026-01-12T23:26:10.214Z" }, + { url = "https://files.pythonhosted.org/packages/10/f3/061bb702465904b6502f7c9081daee34b09ccbaa4f8c94cf43a2a3b6dd6f/polars_runtime_32-1.37.1-cp310-abi3-win_arm64.whl", hash = "sha256:55f2c4847a8d2e267612f564de7b753a4bde3902eaabe7b436a0a4abf75949a0", size = 41001914, upload-time = "2026-01-12T23:26:12.997Z" }, +] + [[package]] name = "pre-commit" version = "4.5.1" @@ -1628,11 +1728,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.23.1" +version = "0.24.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, ] [[package]] @@ -1693,6 +1793,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, ] +[[package]] +name = "pyarrow" +version = "23.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/2f/23e042a5aa99bcb15e794e14030e8d065e00827e846e53a66faec73c7cd6/pyarrow-23.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cbdc2bf5947aa4d462adcf8453cf04aee2f7932653cb67a27acd96e5e8528a67", size = 34281861, upload-time = "2026-01-18T16:13:34.332Z" }, + { url = "https://files.pythonhosted.org/packages/8b/65/1651933f504b335ec9cd8f99463718421eb08d883ed84f0abd2835a16cad/pyarrow-23.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4d38c836930ce15cd31dce20114b21ba082da231c884bdc0a7b53e1477fe7f07", size = 35825067, upload-time = "2026-01-18T16:13:42.549Z" }, + { url = "https://files.pythonhosted.org/packages/84/ec/d6fceaec050c893f4e35c0556b77d4cc9973fcc24b0a358a5781b1234582/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4222ff8f76919ecf6c716175a0e5fddb5599faeed4c56d9ea41a2c42be4998b2", size = 44458539, upload-time = "2026-01-18T16:13:52.975Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d9/369f134d652b21db62fe3ec1c5c2357e695f79eb67394b8a93f3a2b2cffa/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:87f06159cbe38125852657716889296c83c37b4d09a5e58f3d10245fd1f69795", size = 47535889, upload-time = "2026-01-18T16:14:03.693Z" }, + { url = "https://files.pythonhosted.org/packages/a3/95/f37b6a252fdbf247a67a78fb3f61a529fe0600e304c4d07741763d3522b1/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1675c374570d8b91ea6d4edd4608fa55951acd44e0c31bd146e091b4005de24f", size = 48157777, upload-time = "2026-01-18T16:14:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ab/fb94923108c9c6415dab677cf1f066d3307798eafc03f9a65ab4abc61056/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:247374428fde4f668f138b04031a7e7077ba5fa0b5b1722fdf89a017bf0b7ee0", size = 50580441, upload-time = "2026-01-18T16:14:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/ae/78/897ba6337b517fc8e914891e1bd918da1c4eb8e936a553e95862e67b80f6/pyarrow-23.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:de53b1bd3b88a2ee93c9af412c903e57e738c083be4f6392288294513cd8b2c1", size = 27530028, upload-time = "2026-01-18T16:14:27.353Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c0/57fe251102ca834fee0ef69a84ad33cc0ff9d5dfc50f50b466846356ecd7/pyarrow-23.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5574d541923efcbfdf1294a2746ae3b8c2498a2dc6cd477882f6f4e7b1ac08d3", size = 34276762, upload-time = "2026-01-18T16:14:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4e/24130286548a5bc250cbed0b6bbf289a2775378a6e0e6f086ae8c68fc098/pyarrow-23.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:2ef0075c2488932e9d3c2eb3482f9459c4be629aa673b725d5e3cf18f777f8e4", size = 35821420, upload-time = "2026-01-18T16:14:40.699Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/a869e8529d487aa2e842d6c8865eb1e2c9ec33ce2786eb91104d2c3e3f10/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:65666fc269669af1ef1c14478c52222a2aa5c907f28b68fb50a203c777e4f60c", size = 44457412, upload-time = "2026-01-18T16:14:49.051Z" }, + { url = "https://files.pythonhosted.org/packages/36/81/1de4f0edfa9a483bbdf0082a05790bd6a20ed2169ea12a65039753be3a01/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4d85cb6177198f3812db4788e394b757223f60d9a9f5ad6634b3e32be1525803", size = 47534285, upload-time = "2026-01-18T16:14:56.748Z" }, + { url = "https://files.pythonhosted.org/packages/f2/04/464a052d673b5ece074518f27377861662449f3c1fdb39ce740d646fd098/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1a9ff6fa4141c24a03a1a434c63c8fa97ce70f8f36bccabc18ebba905ddf0f17", size = 48157913, upload-time = "2026-01-18T16:15:05.114Z" }, + { url = "https://files.pythonhosted.org/packages/f4/1b/32a4de9856ee6688c670ca2def588382e573cce45241a965af04c2f61687/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:84839d060a54ae734eb60a756aeacb62885244aaa282f3c968f5972ecc7b1ecc", size = 50582529, upload-time = "2026-01-18T16:15:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/d6581f03e9b9e44ea60b52d1750ee1a7678c484c06f939f45365a45f7eef/pyarrow-23.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a149a647dbfe928ce8830a713612aa0b16e22c64feac9d1761529778e4d4eaa5", size = 27542646, upload-time = "2026-01-18T16:15:18.89Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bd/c861d020831ee57609b73ea721a617985ece817684dc82415b0bc3e03ac3/pyarrow-23.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5961a9f646c232697c24f54d3419e69b4261ba8a8b66b0ac54a1851faffcbab8", size = 34189116, upload-time = "2026-01-18T16:15:28.054Z" }, + { url = "https://files.pythonhosted.org/packages/8c/23/7725ad6cdcbaf6346221391e7b3eecd113684c805b0a95f32014e6fa0736/pyarrow-23.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:632b3e7c3d232f41d64e1a4a043fb82d44f8a349f339a1188c6a0dd9d2d47d8a", size = 35803831, upload-time = "2026-01-18T16:15:33.798Z" }, + { url = "https://files.pythonhosted.org/packages/57/06/684a421543455cdc2944d6a0c2cc3425b028a4c6b90e34b35580c4899743/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:76242c846db1411f1d6c2cc3823be6b86b40567ee24493344f8226ba34a81333", size = 44436452, upload-time = "2026-01-18T16:15:41.598Z" }, + { url = "https://files.pythonhosted.org/packages/c6/6f/8f9eb40c2328d66e8b097777ddcf38494115ff9f1b5bc9754ba46991191e/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b73519f8b52ae28127000986bf228fda781e81d3095cd2d3ece76eb5cf760e1b", size = 47557396, upload-time = "2026-01-18T16:15:51.252Z" }, + { url = "https://files.pythonhosted.org/packages/10/6e/f08075f1472e5159553501fde2cc7bc6700944bdabe49a03f8a035ee6ccd/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:068701f6823449b1b6469120f399a1239766b117d211c5d2519d4ed5861f75de", size = 48147129, upload-time = "2026-01-18T16:16:00.299Z" }, + { url = "https://files.pythonhosted.org/packages/7d/82/d5a680cd507deed62d141cc7f07f7944a6766fc51019f7f118e4d8ad0fb8/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1801ba947015d10e23bca9dd6ef5d0e9064a81569a89b6e9a63b59224fd060df", size = 50596642, upload-time = "2026-01-18T16:16:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/a9/26/4f29c61b3dce9fa7780303b86895ec6a0917c9af927101daaaf118fbe462/pyarrow-23.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:52265266201ec25b6839bf6bd4ea918ca6d50f31d13e1cf200b4261cd11dc25c", size = 27660628, upload-time = "2026-01-18T16:16:15.28Z" }, + { url = "https://files.pythonhosted.org/packages/66/34/564db447d083ec7ff93e0a883a597d2f214e552823bfc178a2d0b1f2c257/pyarrow-23.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ad96a597547af7827342ffb3c503c8316e5043bb09b47a84885ce39394c96e00", size = 34184630, upload-time = "2026-01-18T16:16:22.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3a/3999daebcb5e6119690c92a621c4d78eef2ffba7a0a1b56386d2875fcd77/pyarrow-23.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b9edf990df77c2901e79608f08c13fbde60202334a4fcadb15c1f57bf7afee43", size = 35796820, upload-time = "2026-01-18T16:16:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/39195233056c6a8d0976d7d1ac1cd4fe21fb0ec534eca76bc23ef3f60e11/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:36d1b5bc6ddcaff0083ceec7e2561ed61a51f49cce8be079ee8ed406acb6fdef", size = 44438735, upload-time = "2026-01-18T16:16:38.79Z" }, + { url = "https://files.pythonhosted.org/packages/2c/41/6a7328ee493527e7afc0c88d105ecca69a3580e29f2faaeac29308369fd7/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4292b889cd224f403304ddda8b63a36e60f92911f89927ec8d98021845ea21be", size = 47557263, upload-time = "2026-01-18T16:16:46.248Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ee/34e95b21ee84db494eae60083ddb4383477b31fb1fd19fd866d794881696/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dfd9e133e60eaa847fd80530a1b89a052f09f695d0b9c34c235ea6b2e0924cf7", size = 48153529, upload-time = "2026-01-18T16:16:53.412Z" }, + { url = "https://files.pythonhosted.org/packages/52/88/8a8d83cea30f4563efa1b7bf51d241331ee5cd1b185a7e063f5634eca415/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832141cc09fac6aab1cd3719951d23301396968de87080c57c9a7634e0ecd068", size = 50598851, upload-time = "2026-01-18T16:17:01.133Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4c/2929c4be88723ba025e7b3453047dc67e491c9422965c141d24bab6b5962/pyarrow-23.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:7a7d067c9a88faca655c71bcc30ee2782038d59c802d57950826a07f60d83c4c", size = 27577747, upload-time = "2026-01-18T16:18:02.413Z" }, + { url = "https://files.pythonhosted.org/packages/64/52/564a61b0b82d72bd68ec3aef1adda1e3eba776f89134b9ebcb5af4b13cb6/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ce9486e0535a843cf85d990e2ec5820a47918235183a5c7b8b97ed7e92c2d47d", size = 34446038, upload-time = "2026-01-18T16:17:07.861Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/232d4f9855fd1de0067c8a7808a363230d223c83aeee75e0fe6eab851ba9/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:075c29aeaa685fd1182992a9ed2499c66f084ee54eea47da3eb76e125e06064c", size = 35921142, upload-time = "2026-01-18T16:17:15.401Z" }, + { url = "https://files.pythonhosted.org/packages/96/f2/60af606a3748367b906bb82d41f0032e059f075444445d47e32a7ff1df62/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:799965a5379589510d888be3094c2296efd186a17ca1cef5b77703d4d5121f53", size = 44490374, upload-time = "2026-01-18T16:17:23.93Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/7731543050a678ea3a413955a2d5d80d2a642f270aa57a3cb7d5a86e3f46/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef7cac8fe6fccd8b9e7617bfac785b0371a7fe26af59463074e4882747145d40", size = 47527896, upload-time = "2026-01-18T16:17:33.393Z" }, + { url = "https://files.pythonhosted.org/packages/5a/90/f3342553b7ac9879413aed46500f1637296f3c8222107523a43a1c08b42a/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15a414f710dc927132dd67c361f78c194447479555af57317066ee5116b90e9e", size = 48210401, upload-time = "2026-01-18T16:17:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/f3/da/9862ade205ecc46c172b6ce5038a74b5151c7401e36255f15975a45878b2/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e0d2e6915eca7d786be6a77bf227fbc06d825a75b5b5fe9bcbef121dec32685", size = 50579677, upload-time = "2026-01-18T16:17:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4c/f11f371f5d4740a5dafc2e11c76bcf42d03dfdb2d68696da97de420b6963/pyarrow-23.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4b317ea6e800b5704e5e5929acb6e2dc13e9276b708ea97a39eb8b345aa2658b", size = 27631889, upload-time = "2026-01-18T16:17:56.55Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/15aec78bcf43a0c004067bd33eb5352836a29a49db8581fc56f2b6ca88b7/pyarrow-23.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:20b187ed9550d233a872074159f765f52f9d92973191cd4b93f293a19efbe377", size = 34213265, upload-time = "2026-01-18T16:18:07.904Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/deb2c594bbba41c37c5d9aa82f510376998352aa69dfcb886cb4b18ad80f/pyarrow-23.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:18ec84e839b493c3886b9b5e06861962ab4adfaeb79b81c76afbd8d84c7d5fda", size = 35819211, upload-time = "2026-01-18T16:18:13.94Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/ee82af693cb7b5b2b74f6524cdfede0e6ace779d7720ebca24d68b57c36b/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e438dd3f33894e34fd02b26bd12a32d30d006f5852315f611aa4add6c7fab4bc", size = 44502313, upload-time = "2026-01-18T16:18:20.367Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/95c61ad82236495f3c31987e85135926ba3ec7f3819296b70a68d8066b49/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a244279f240c81f135631be91146d7fa0e9e840e1dfed2aba8483eba25cd98e6", size = 47585886, upload-time = "2026-01-18T16:18:27.544Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6e/a72d901f305201802f016d015de1e05def7706fff68a1dedefef5dc7eff7/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c4692e83e42438dba512a570c6eaa42be2f8b6c0f492aea27dec54bdc495103a", size = 48207055, upload-time = "2026-01-18T16:18:35.425Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/5de029c537630ca18828db45c30e2a78da03675a70ac6c3528203c416fe3/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae7f30f898dfe44ea69654a35c93e8da4cef6606dc4c72394068fd95f8e9f54a", size = 50619812, upload-time = "2026-01-18T16:18:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/59/8d/2af846cd2412e67a087f5bda4a8e23dfd4ebd570f777db2e8686615dafc1/pyarrow-23.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:5b86bb649e4112fb0614294b7d0a175c7513738876b89655605ebb87c804f861", size = 28263851, upload-time = "2026-01-18T16:19:38.567Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7f/caab863e587041156f6786c52e64151b7386742c8c27140f637176e9230e/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ebc017d765d71d80a3f8584ca0566b53e40464586585ac64176115baa0ada7d3", size = 34463240, upload-time = "2026-01-18T16:18:49.755Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fa/3a5b8c86c958e83622b40865e11af0857c48ec763c11d472c87cd518283d/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:0800cc58a6d17d159df823f87ad66cefebf105b982493d4bad03ee7fab84b993", size = 35935712, upload-time = "2026-01-18T16:18:55.626Z" }, + { url = "https://files.pythonhosted.org/packages/c5/08/17a62078fc1a53decb34a9aa79cf9009efc74d63d2422e5ade9fed2f99e3/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3a7c68c722da9bb5b0f8c10e3eae71d9825a4b429b40b32709df5d1fa55beb3d", size = 44503523, upload-time = "2026-01-18T16:19:03.958Z" }, + { url = "https://files.pythonhosted.org/packages/cc/70/84d45c74341e798aae0323d33b7c39194e23b1abc439ceaf60a68a7a969a/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:bd5556c24622df90551063ea41f559b714aa63ca953db884cfb958559087a14e", size = 47542490, upload-time = "2026-01-18T16:19:11.208Z" }, + { url = "https://files.pythonhosted.org/packages/61/d9/d1274b0e6f19e235de17441e53224f4716574b2ca837022d55702f24d71d/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54810f6e6afc4ffee7c2e0051b61722fbea9a4961b46192dcfae8ea12fa09059", size = 48233605, upload-time = "2026-01-18T16:19:19.544Z" }, + { url = "https://files.pythonhosted.org/packages/39/07/e4e2d568cb57543d84482f61e510732820cddb0f47c4bb7df629abfed852/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:14de7d48052cf4b0ed174533eafa3cfe0711b8076ad70bede32cf59f744f0d7c", size = 50603979, upload-time = "2026-01-18T16:19:26.717Z" }, + { url = "https://files.pythonhosted.org/packages/72/9c/47693463894b610f8439b2e970b82ef81e9599c757bf2049365e40ff963c/pyarrow-23.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:427deac1f535830a744a4f04a6ac183a64fcac4341b3f618e693c41b7b98d2b0", size = 28338905, upload-time = "2026-01-18T16:19:32.93Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -1710,10 +1867,12 @@ dependencies = [ { name = "accessible-pygments" }, { name = "babel" }, { name = "beautifulsoup4" }, - { name = "docutils" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pygments" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/20/bb50f9de3a6de69e6abd6b087b52fa2418a0418b19597601605f855ad044/pydata_sphinx_theme-0.16.1.tar.gz", hash = "sha256:a08b7f0b7f70387219dc659bff0893a7554d5eb39b59d3b8ef37b8401b7642d7", size = 2412693, upload-time = "2024-12-17T10:53:39.537Z" } @@ -2001,18 +2160,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] -[[package]] -name = "roman-numerals-py" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "roman-numerals", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/de96fca640f4f656eb79bbee0e79aeec52e3e0e359f8a3e6a0d366378b64/roman_numerals_py-4.1.0.tar.gz", hash = "sha256:f5d7b2b4ca52dd855ef7ab8eb3590f428c0b1ea480736ce32b01fef2a5f8daf9", size = 4274, upload-time = "2025-12-17T18:25:41.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/2c/daca29684cbe9fd4bc711f8246da3c10adca1ccc4d24436b17572eb2590e/roman_numerals_py-4.1.0-py3-none-any.whl", hash = "sha256:553114c1167141c1283a51743759723ecd05604a1b6b507225e91dc1a6df0780", size = 4547, upload-time = "2025-12-17T18:25:40.136Z" }, -] - [[package]] name = "rpds-py" version = "0.30.0" @@ -2137,37 +2284,37 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, - { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, - { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, - { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, - { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, - { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, - { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, - { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, - { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, - { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, - { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, - { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, ] [[package]] name = "send2trash" -version = "2.0.0" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/6e/421803dec0c0dfbf5a27e66491ebe6643a461e4f90422f00ea4c68ae24aa/send2trash-2.0.0.tar.gz", hash = "sha256:1761421da3f9930bfe51ed7c45343948573383ad4c27e3acebc91be324e7770d", size = 17206, upload-time = "2025-12-31T04:12:48.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c5/f0/184b4b5f8d00f2a92cf96eec8967a3d550b52cf94362dad1100df9e48d57/send2trash-2.1.0.tar.gz", hash = "sha256:1c72b39f09457db3c05ce1d19158c2cbef4c32b8bedd02c155e49282b7ea7459", size = 17255, upload-time = "2026-01-14T06:27:36.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/5a/f2f2e5eda25579f754acd83399c522ee03d6acbe001dfe53c8a1ec928b44/send2trash-2.0.0-py3-none-any.whl", hash = "sha256:e70d5ce41dbb890882cc78bc25d137478330b39a391e756fadf82e34da4d85b8", size = 17642, upload-time = "2025-12-31T04:12:45.336Z" }, + { url = "https://files.pythonhosted.org/packages/1c/78/504fdd027da3b84ff1aecd9f6957e65f35134534ccc6da8628eb71e76d3f/send2trash-2.1.0-py3-none-any.whl", hash = "sha256:0da2f112e6d6bb22de6aa6daa7e144831a4febf2a87261451c4ad849fe9a873c", size = 17610, upload-time = "2026-01-14T06:27:35.218Z" }, ] [[package]] @@ -2199,11 +2346,11 @@ wheels = [ [[package]] name = "soupsieve" -version = "2.8.1" +version = "2.8.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/23/adf3796d740536d63a6fbda113d07e60c734b6ed5d3058d1e47fc0495e47/soupsieve-2.8.1.tar.gz", hash = "sha256:4cf733bc50fa805f5df4b8ef4740fc0e0fa6218cf3006269afd3f9d6d80fd350", size = 117856, upload-time = "2025-12-18T13:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/93/f2/21d6ca70c3cf35d01ae9e01be534bf6b6b103c157a728082a5028350c310/soupsieve-2.8.2.tar.gz", hash = "sha256:78a66b0fdee2ab40b7199dc3e747ee6c6e231899feeaae0b9b98a353afd48fd8", size = 118601, upload-time = "2026-01-18T16:21:31.09Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/f3/b67d6ea49ca9154453b6d70b34ea22f3996b9fa55da105a79d8732227adc/soupsieve-2.8.1-py3-none-any.whl", hash = "sha256:a11fe2a6f3d76ab3cf2de04eb339c1be5b506a8a47f2ceb6d139803177f85434", size = 36710, upload-time = "2025-12-18T13:50:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9a/b4450ccce353e2430621b3bb571899ffe1033d5cd72c9e065110f95b1a63/soupsieve-2.8.2-py3-none-any.whl", hash = "sha256:0f4c2f6b5a5fb97a641cf69c0bd163670a0e45e6d6c01a2107f93a6a6f93c51a", size = 37016, upload-time = "2026-01-18T16:21:29.7Z" }, ] [[package]] @@ -2217,7 +2364,7 @@ dependencies = [ { name = "alabaster", marker = "python_full_version < '3.11'" }, { name = "babel", marker = "python_full_version < '3.11'" }, { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "imagesize", marker = "python_full_version < '3.11'" }, { name = "jinja2", marker = "python_full_version < '3.11'" }, { name = "packaging", marker = "python_full_version < '3.11'" }, @@ -2239,35 +2386,65 @@ wheels = [ [[package]] name = "sphinx" -version = "8.2.3" +version = "9.0.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version >= '3.12' and python_full_version < '3.14'", "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "imagesize", marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, + { name = "alabaster", marker = "python_full_version == '3.11.*'" }, + { name = "babel", marker = "python_full_version == '3.11.*'" }, + { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "imagesize", marker = "python_full_version == '3.11.*'" }, + { name = "jinja2", marker = "python_full_version == '3.11.*'" }, + { name = "packaging", marker = "python_full_version == '3.11.*'" }, + { name = "pygments", marker = "python_full_version == '3.11.*'" }, + { name = "requests", marker = "python_full_version == '3.11.*'" }, + { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, + { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/3f/4bbd76424c393caead2e1eb89777f575dee5c8653e2d4b6afd7a564f5974/sphinx-9.0.4-py3-none-any.whl", hash = "sha256:5bebc595a5e943ea248b99c13814c1c5e10b3ece718976824ffa7959ff95fffb", size = 3917713, upload-time = "2025-12-04T07:45:24.944Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876, upload-time = "2025-03-02T22:31:59.658Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/53/136e9eca6e0b9dc0e1962e2c908fbea2e5ac000c2a2fbd9a35797958c48b/sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3", size = 3589741, upload-time = "2025-03-02T22:31:56.836Z" }, + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version >= '3.12' and python_full_version < '3.14'", +] +dependencies = [ + { name = "alabaster", marker = "python_full_version >= '3.12'" }, + { name = "babel", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "imagesize", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pygments", marker = "python_full_version >= '3.12'" }, + { name = "requests", marker = "python_full_version >= '3.12'" }, + { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, + { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, + { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] [[package]] @@ -2276,11 +2453,12 @@ version = "3.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "astroid", version = "3.3.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "astroid", version = "4.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "astroid", version = "4.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "jinja2" }, { name = "pyyaml" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/ad/c627976d5f4d812b203ef1136108bbd81ef9bbbfd3f700f1295c322c22e6/sphinx_autoapi-3.6.1.tar.gz", hash = "sha256:1ff2992b7d5e39ccf92413098a376e0f91e7b4ca532c4f3e71298dbc8a4a9900", size = 55456, upload-time = "2025-10-06T16:21:22.888Z" } wheels = [ @@ -2360,9 +2538,9 @@ name = "tab-err" version = "0.2.1" source = { editable = "." } dependencies = [ + { name = "narwhals" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pandas" }, + { name = "numpy", version = "2.4.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] [package.dev-dependencies] @@ -2372,20 +2550,25 @@ ci = [ dev = [ { name = "jupyterlab" }, { name = "mypy" }, - { name = "myst-parser" }, + { name = "myst-parser", version = "4.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "myst-parser", version = "5.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas" }, + { name = "polars" }, { name = "pre-commit" }, + { name = "pyarrow" }, { name = "pydata-sphinx-theme" }, { name = "pytest" }, { name = "ruff" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx-autoapi" }, { name = "ty" }, ] [package.metadata] requires-dist = [ - { name = "numpy", specifier = ">=2.2.6,<2.5.0" }, - { name = "pandas", specifier = ">=2.3.3,<2.4.0" }, + { name = "narwhals", specifier = ">=1.30.0" }, + { name = "numpy", specifier = ">=1.24.0" }, ] [package.metadata.requires-dev] @@ -2394,7 +2577,10 @@ dev = [ { name = "jupyterlab", specifier = ">=4.3.2,<5.0.0" }, { name = "mypy", specifier = ">=1.14.1,<2.0.0" }, { name = "myst-parser", marker = "python_full_version >= '3.10'", specifier = ">=4.0.0" }, + { name = "pandas", specifier = ">=2.0.0" }, + { name = "polars", specifier = ">=1.0.0" }, { name = "pre-commit", specifier = ">=4.0.1,<5.0.0" }, + { name = "pyarrow", specifier = ">=14.0.0" }, { name = "pydata-sphinx-theme", specifier = ">=0.16.1,<0.17.0" }, { name = "pytest", specifier = ">=8.3.5,<9.0.0" }, { name = "ruff", specifier = ">=0.9.8" }, @@ -2431,51 +2617,56 @@ wheels = [ [[package]] name = "tomli" -version = "2.3.0" +version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +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/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { 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" }, ] [[package]] @@ -2508,27 +2699,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/9d/59e955cc39206a0d58df5374808785c45ec2a8a2a230eb1638fbb4fe5c5d/ty-0.0.8.tar.gz", hash = "sha256:352ac93d6e0050763be57ad1e02087f454a842887e618ec14ac2103feac48676", size = 4828477, upload-time = "2025-12-29T13:50:07.193Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/2b/dd61f7e50a69c72f72c625d026e9ab64a0db62b2dd32e7426b520e2429c6/ty-0.0.8-py3-none-linux_armv6l.whl", hash = "sha256:a289d033c5576fa3b4a582b37d63395edf971cdbf70d2d2e6b8c95638d1a4fcd", size = 9853417, upload-time = "2025-12-29T13:50:08.979Z" }, - { url = "https://files.pythonhosted.org/packages/90/72/3f1d3c64a049a388e199de4493689a51fc6aa5ff9884c03dea52b4966657/ty-0.0.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:788ea97dc8153a94e476c4d57b2551a9458f79c187c4aba48fcb81f05372924a", size = 9657890, upload-time = "2025-12-29T13:50:27.867Z" }, - { url = "https://files.pythonhosted.org/packages/71/d1/08ac676bd536de3c2baba0deb60e67b3196683a2fabebfd35659d794b5e9/ty-0.0.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1b5f1f3d3e230f35a29e520be7c3d90194a5229f755b721e9092879c00842d31", size = 9180129, upload-time = "2025-12-29T13:50:22.842Z" }, - { url = "https://files.pythonhosted.org/packages/af/93/610000e2cfeea1875900f73a375ba917624b0a008d4b8a6c18c894c8dbbc/ty-0.0.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6da9ed377fbbcec0a3b60b2ca5fd30496e15068f47cef2344ba87923e78ba996", size = 9683517, upload-time = "2025-12-29T13:50:18.658Z" }, - { url = "https://files.pythonhosted.org/packages/05/04/bef50ba7d8580b0140be597de5cc0ba9a63abe50d3f65560235f23658762/ty-0.0.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7d0a2bdce5e701d19eb8d46d9da0fe31340f079cecb7c438f5ac6897c73fc5ba", size = 9676279, upload-time = "2025-12-29T13:50:25.207Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b9/2aff1ef1f41b25898bc963173ae67fc8f04ca666ac9439a9c4e78d5cc0ff/ty-0.0.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef9078799d26d3cc65366e02392e2b78f64f72911b599e80a8497d2ec3117ddb", size = 10073015, upload-time = "2025-12-29T13:50:35.422Z" }, - { url = "https://files.pythonhosted.org/packages/df/0e/9feb6794b6ff0a157c3e6a8eb6365cbfa3adb9c0f7976e2abdc48615dd72/ty-0.0.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:54814ac39b4ab67cf111fc0a236818155cf49828976152378347a7678d30ee89", size = 10961649, upload-time = "2025-12-29T13:49:58.717Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3b/faf7328b14f00408f4f65c9d01efe52e11b9bcc4a79e06187b370457b004/ty-0.0.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4baf0a80398e8b6c68fa36ff85045a50ede1906cd4edb41fb4fab46d471f1d4", size = 10676190, upload-time = "2025-12-29T13:50:01.11Z" }, - { url = "https://files.pythonhosted.org/packages/64/a5/cfeca780de7eeab7852c911c06a84615a174d23e9ae08aae42a645771094/ty-0.0.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac8e23c3faefc579686799ef1649af8d158653169ad5c3a7df56b152781eeb67", size = 10438641, upload-time = "2025-12-29T13:50:29.664Z" }, - { url = "https://files.pythonhosted.org/packages/0e/8d/8667c7e0ac9f13c461ded487c8d7350f440cd39ba866d0160a8e1b1efd6c/ty-0.0.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b558a647a073d0c25540aaa10f8947de826cb8757d034dd61ecf50ab8dbd77bf", size = 10214082, upload-time = "2025-12-29T13:50:31.531Z" }, - { url = "https://files.pythonhosted.org/packages/f8/11/e563229870e2c1d089e7e715c6c3b7605a34436dddf6f58e9205823020c2/ty-0.0.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:8c0104327bf480508bd81f320e22074477df159d9eff85207df39e9c62ad5e96", size = 9664364, upload-time = "2025-12-29T13:50:05.443Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ad/05b79b778bf5237bcd7ee08763b226130aa8da872cbb151c8cfa2e886203/ty-0.0.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:496f1cb87261dd1a036a5609da80ee13de2e6ee4718a661bfa2afb91352fe528", size = 9679440, upload-time = "2025-12-29T13:50:11.289Z" }, - { url = "https://files.pythonhosted.org/packages/12/b5/23ba887769c4a7b8abfd1b6395947dc3dcc87533fbf86379d3a57f87ae8f/ty-0.0.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2c488031f92a075ae39d13ac6295fdce2141164ec38c5d47aa8dc24ee3afa37e", size = 9808201, upload-time = "2025-12-29T13:50:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/f8/90/5a82ac0a0707db55376922aed80cd5fca6b2e6d6e9bcd8c286e6b43b4084/ty-0.0.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90d6f08c5982fa3e802b8918a32e326153519077b827f91c66eea4913a86756a", size = 10313262, upload-time = "2025-12-29T13:50:03.306Z" }, - { url = "https://files.pythonhosted.org/packages/14/f7/ff97f37f0a75db9495ddbc47738ec4339837867c4bfa145bdcfbd0d1eb2f/ty-0.0.8-py3-none-win32.whl", hash = "sha256:d7f460ad6fc9325e9cc8ea898949bbd88141b4609d1088d7ede02ce2ef06e776", size = 9254675, upload-time = "2025-12-29T13:50:33.35Z" }, - { url = "https://files.pythonhosted.org/packages/af/51/eba5d83015e04630002209e3590c310a0ff1d26e1815af204a322617a42e/ty-0.0.8-py3-none-win_amd64.whl", hash = "sha256:1641fb8dedc3d2da43279d21c3c7c1f80d84eae5c264a1e8daa544458e433c19", size = 10131382, upload-time = "2025-12-29T13:50:13.719Z" }, - { url = "https://files.pythonhosted.org/packages/38/1c/0d8454ff0f0f258737ecfe84f6e508729191d29663b404832f98fa5626b7/ty-0.0.8-py3-none-win_arm64.whl", hash = "sha256:ec74f022f315bede478ecae1277a01ab618e6500c1d68450d7883f5cd6ed554a", size = 9636374, upload-time = "2025-12-29T13:50:16.344Z" }, +version = "0.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/78/ba1a4ad403c748fbba8be63b7e774a90e80b67192f6443d624c64fe4aaab/ty-0.0.12.tar.gz", hash = "sha256:cd01810e106c3b652a01b8f784dd21741de9fdc47bd595d02c122a7d5cefeee7", size = 4981303, upload-time = "2026-01-14T22:30:48.537Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/8f/c21314d074dda5fb13d3300fa6733fd0d8ff23ea83a721818740665b6314/ty-0.0.12-py3-none-linux_armv6l.whl", hash = "sha256:eb9da1e2c68bd754e090eab39ed65edf95168d36cbeb43ff2bd9f86b4edd56d1", size = 9614164, upload-time = "2026-01-14T22:30:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/09/28/f8a4d944d13519d70c486e8f96d6fa95647ac2aa94432e97d5cfec1f42f6/ty-0.0.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c181f42aa19b0ed7f1b0c2d559980b1f1d77cc09419f51c8321c7ddf67758853", size = 9542337, upload-time = "2026-01-14T22:30:05.687Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9c/f576e360441de7a8201daa6dc4ebc362853bc5305e059cceeb02ebdd9a48/ty-0.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f829e1eecd39c3e1b032149db7ae6a3284f72fc36b42436e65243a9ed1173db", size = 8909582, upload-time = "2026-01-14T22:30:46.089Z" }, + { url = "https://files.pythonhosted.org/packages/d6/13/0898e494032a5d8af3060733d12929e3e7716db6c75eac63fa125730a3e7/ty-0.0.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45162e7826e1789cf3374627883cdeb0d56b82473a0771923e4572928e90be3", size = 9384932, upload-time = "2026-01-14T22:30:13.769Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1a/b35b6c697008a11d4cedfd34d9672db2f0a0621ec80ece109e13fca4dfef/ty-0.0.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d11fec40b269bec01e751b2337d1c7ffa959a2c2090a950d7e21c2792442cccd", size = 9453140, upload-time = "2026-01-14T22:30:11.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/71c9edbc79a3c88a0711324458f29c7dbf6c23452c6e760dc25725483064/ty-0.0.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09d99e37e761a4d2651ad9d5a610d11235fbcbf35dc6d4bc04abf54e7cf894f1", size = 9960680, upload-time = "2026-01-14T22:30:33.621Z" }, + { url = "https://files.pythonhosted.org/packages/0e/75/39375129f62dd22f6ad5a99cd2a42fd27d8b91b235ce2db86875cdad397d/ty-0.0.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d9ca0cdb17bd37397da7b16a7cd23423fc65c3f9691e453ad46c723d121225a1", size = 10904518, upload-time = "2026-01-14T22:30:08.464Z" }, + { url = "https://files.pythonhosted.org/packages/32/5e/26c6d88fafa11a9d31ca9f4d12989f57782ec61e7291d4802d685b5be118/ty-0.0.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcf2757b905e7eddb7e456140066335b18eb68b634a9f72d6f54a427ab042c64", size = 10525001, upload-time = "2026-01-14T22:30:16.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a5/2f0b91894af13187110f9ad7ee926d86e4e6efa755c9c88a820ed7f84c85/ty-0.0.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00cf34c1ebe1147efeda3021a1064baa222c18cdac114b7b050bbe42deb4ca80", size = 10307103, upload-time = "2026-01-14T22:30:41.221Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/13d0410827e4bc713ebb7fdaf6b3590b37dcb1b82e0a81717b65548f2442/ty-0.0.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3a655bd869352e9a22938d707631ac9fbca1016242b1f6d132d78f347c851", size = 10072737, upload-time = "2026-01-14T22:30:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dd/fc36d8bac806c74cf04b4ca735bca14d19967ca84d88f31e121767880df1/ty-0.0.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4658e282c7cb82be304052f8f64f9925f23c3c4f90eeeb32663c74c4b095d7ba", size = 9368726, upload-time = "2026-01-14T22:30:18.683Z" }, + { url = "https://files.pythonhosted.org/packages/54/70/9e8e461647550f83e2fe54bc632ccbdc17a4909644783cdbdd17f7296059/ty-0.0.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c167d838eaaa06e03bb66a517f75296b643d950fbd93c1d1686a187e5a8dbd1f", size = 9454704, upload-time = "2026-01-14T22:30:22.759Z" }, + { url = "https://files.pythonhosted.org/packages/04/9b/6292cf7c14a0efeca0539cf7d78f453beff0475cb039fbea0eb5d07d343d/ty-0.0.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2956e0c9ab7023533b461d8a0e6b2ea7b78e01a8dde0688e8234d0fce10c4c1c", size = 9649829, upload-time = "2026-01-14T22:30:31.234Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/472a5d2013371e4870886cff791c94abdf0b92d43d305dd0f8e06b6ff719/ty-0.0.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c6a3fd7479580009f21002f3828320621d8a82d53b7ba36993234e3ccad58c8", size = 10162814, upload-time = "2026-01-14T22:30:36.174Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/2ecbe56826759845a7c21d80aa28187865ea62bc9757b056f6cbc06f78ed/ty-0.0.12-py3-none-win32.whl", hash = "sha256:a91c24fd75c0f1796d8ede9083e2c0ec96f106dbda73a09fe3135e075d31f742", size = 9140115, upload-time = "2026-01-14T22:30:38.903Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6d/d9531eff35a5c0ec9dbc10231fac21f9dd6504814048e81d6ce1c84dc566/ty-0.0.12-py3-none-win_amd64.whl", hash = "sha256:df151894be55c22d47068b0f3b484aff9e638761e2267e115d515fcc9c5b4a4b", size = 9884532, upload-time = "2026-01-14T22:30:25.112Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f3/20b49e75967023b123a221134548ad7000f9429f13fdcdda115b4c26305f/ty-0.0.12-py3-none-win_arm64.whl", hash = "sha256:cea99d334b05629de937ce52f43278acf155d3a316ad6a35356635f886be20ea", size = 9313974, upload-time = "2026-01-14T22:30:27.44Z" }, ] [[package]] @@ -2560,16 +2751,16 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.2" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] name = "virtualenv" -version = "20.35.4" +version = "20.36.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -2577,9 +2768,9 @@ dependencies = [ { name = "platformdirs" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, ] [[package]]