From 115a695a16e6a94767ec6058fe9ed9588e694e56 Mon Sep 17 00:00:00 2001 From: Ceceliachenen <162673161+Ceceliachenen@users.noreply.github.com> Date: Wed, 5 Jun 2024 19:48:55 +0800 Subject: [PATCH] Add tabular reader (#47) * tabular reader * tabular reader * tabular reader * tabular reader * tabular reader * tabular reader --------- Co-authored-by: Yue Fei <59813791+moria97@users.noreply.github.com> --- poetry.lock | 244 ++++++++++-------- pyproject.toml | 1 + src/pai_rag/docs/tabular_doc.md | 76 ++++++ .../integrations/readers/pai_csv_reader.py | 151 +++++++++++ .../integrations/readers/pai_excel_reader.py | 121 +++++++++ .../modules/datareader/datareader_factory.py | 11 + tests/data_readers/test_csv_reader.py | 48 ++++ tests/data_readers/test_excel_reader.py | 32 +++ ...45\255\230\347\216\207_csv_two_header.csv" | 9 + ...255\230\347\216\207_excel_two_header.xlsx" | Bin 0 -> 9147 bytes 10 files changed, 579 insertions(+), 114 deletions(-) create mode 100644 src/pai_rag/docs/tabular_doc.md create mode 100644 src/pai_rag/integrations/readers/pai_csv_reader.py create mode 100644 src/pai_rag/integrations/readers/pai_excel_reader.py create mode 100644 tests/data_readers/test_csv_reader.py create mode 100644 tests/data_readers/test_excel_reader.py create mode 100644 "tests/testdata/data/csv_data/30\345\244\251\347\225\231\345\255\230\347\216\207_csv_two_header.csv" create mode 100644 "tests/testdata/data/excel_data/30\345\244\251\347\225\231\345\255\230\347\216\207_excel_two_header.xlsx" diff --git a/poetry.lock b/poetry.lock index 9bfd357d..4e338697 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1282,13 +1282,13 @@ files = [ [[package]] name = "duckduckgo-search" -version = "6.1.4" +version = "6.1.5" description = "Search for words, documents, images, news, maps and text translation using the DuckDuckGo.com search engine." optional = false python-versions = ">=3.8" files = [ - {file = "duckduckgo_search-6.1.4-py3-none-any.whl", hash = "sha256:2248def55da5982a5d055a22972e880646de6b6e5f77d1e4fdb971aed151bd05"}, - {file = "duckduckgo_search-6.1.4.tar.gz", hash = "sha256:4b7baf2aebf05302bbb6791df05a968c4c5a3835e5f3f422b5048fed230d2699"}, + {file = "duckduckgo_search-6.1.5-py3-none-any.whl", hash = "sha256:f0a18fe5f20323ba6bb11865ce32d4520bb90086a6ae62f5da510865f5a7dca8"}, + {file = "duckduckgo_search-6.1.5.tar.gz", hash = "sha256:10e5c4d09a4243fd9d85007dc4fe637456c3c3995fd2e1e9b49ffd6f75bb0afb"}, ] [package.dependencies] @@ -1828,17 +1828,17 @@ tool = ["click (>=6.0.0)"] [[package]] name = "googleapis-common-protos" -version = "1.63.0" +version = "1.63.1" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e"}, - {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"}, + {file = "googleapis-common-protos-1.63.1.tar.gz", hash = "sha256:c6442f7a0a6b2a80369457d79e6672bb7dcbaab88e0848302497e3ec80780a6a"}, + {file = "googleapis_common_protos-1.63.1-py2.py3-none-any.whl", hash = "sha256:0e1c2cdfcbc354b76e4a211a35ea35d6926a835cba1377073c4861db904a1877"}, ] [package.dependencies] -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] @@ -3963,13 +3963,13 @@ sympy = "*" [[package]] name = "openai" -version = "1.30.5" +version = "1.31.0" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.30.5-py3-none-any.whl", hash = "sha256:2ad95e926de0d2e09cde632a9204b0a6dca4a03c2cdcc84329b01f355784355a"}, - {file = "openai-1.30.5.tar.gz", hash = "sha256:5366562eb2c5917e6116ae0391b7ae6e3acd62b0ae3f565ada32b35d8fcfa106"}, + {file = "openai-1.31.0-py3-none-any.whl", hash = "sha256:82044ee3122113f2a468a1f308a8882324d09556ba5348687c535d3655ee331c"}, + {file = "openai-1.31.0.tar.gz", hash = "sha256:54ae0625b005d6a3b895db2b8438dae1059cffff0cd262a26e9015c13a29ab06"}, ] [package.dependencies] @@ -3986,48 +3986,48 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] name = "opencv-python" -version = "4.9.0.80" +version = "4.10.0.82" description = "Wrapper package for OpenCV python bindings." optional = false python-versions = ">=3.6" files = [ - {file = "opencv-python-4.9.0.80.tar.gz", hash = "sha256:1a9f0e6267de3a1a1db0c54213d022c7c8b5b9ca4b580e80bdc58516c922c9e1"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:7e5f7aa4486651a6ebfa8ed4b594b65bd2d2f41beeb4241a3e4b1b85acbbbadb"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71dfb9555ccccdd77305fc3dcca5897fbf0cf28b297c51ee55e079c065d812a3"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b34a52e9da36dda8c151c6394aed602e4b17fa041df0b9f5b93ae10b0fcca2a"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4088cab82b66a3b37ffc452976b14a3c599269c247895ae9ceb4066d8188a57"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:dcf000c36dd1651118a2462257e3a9e76db789a78432e1f303c7bac54f63ef6c"}, - {file = "opencv_python-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:3f16f08e02b2a2da44259c7cc712e779eff1dd8b55fdb0323e8cab09548086c0"}, + {file = "opencv-python-4.10.0.82.tar.gz", hash = "sha256:dbc021eaa310c4145c47cd648cb973db69bb5780d6e635386cd53d3ea76bd2d5"}, + {file = "opencv_python-4.10.0.82-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:5f78652339957ec24b80a782becfb32f822d2008a865512121fad8c3ce233e9a"}, + {file = "opencv_python-4.10.0.82-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:e6be19a0615aa8c4e0d34e0c7b133e26e386f4b7e9b557b69479104ab2c133ec"}, + {file = "opencv_python-4.10.0.82-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b49e530f7fd86f671514b39ffacdf5d14ceb073bc79d0de46bbb6b0cad78eaf"}, + {file = "opencv_python-4.10.0.82-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955c5ce8ac90c9e4636ad7f5c0d9c75b80abbe347182cfd09b0e3eec6e50472c"}, + {file = "opencv_python-4.10.0.82-cp37-abi3-win32.whl", hash = "sha256:ff54adc9e4daaf438e669664af08bec4a268c7b7356079338b8e4fae03810f2c"}, + {file = "opencv_python-4.10.0.82-cp37-abi3-win_amd64.whl", hash = "sha256:2e3c2557b176f1e528417520a52c0600a92c1bb1c359f3df8e6411ab4293f065"}, ] [package.dependencies] numpy = [ - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, ] [[package]] name = "opencv-python-headless" -version = "4.9.0.80" +version = "4.10.0.82" description = "Wrapper package for OpenCV python bindings." optional = false python-versions = ">=3.6" files = [ - {file = "opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958"}, - {file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a"}, - {file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6"}, - {file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d"}, - {file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df"}, - {file = "opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670"}, - {file = "opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c"}, + {file = "opencv-python-headless-4.10.0.82.tar.gz", hash = "sha256:de9e742c1b9540816fbd115b0b03841d41ed0c65566b0d7a5371f98b131b7e6d"}, + {file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a09ed50ba21cc5bf5d436cb0e784ad09c692d6b1d1454252772f6c8f2c7b4088"}, + {file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:977a5fd21e1fe0d3d2134887db4441f8725abeae95150126302f31fcd9f548fa"}, + {file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4ec6755838b0be12510bfc9ffb014779c612418f11f4f7e6f505c36124a3aa"}, + {file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a37fa5276967ecf6eb297295b16b28b7a2eb3b568ca0ee469fb1a5954de298"}, + {file = "opencv_python_headless-4.10.0.82-cp37-abi3-win32.whl", hash = "sha256:94736e9b322d13db4768fd35588ad5e8995e78e207263076bfbee18aac835ad5"}, + {file = "opencv_python_headless-4.10.0.82-cp37-abi3-win_amd64.whl", hash = "sha256:c1822fa23d1641c0249ed5eb906f4c385f7959ff1bd601a776d56b0c18914af4"}, ] [package.dependencies] numpy = [ - {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.23.5", markers = "python_version >= \"3.11\""}, ] [[package]] @@ -4394,8 +4394,8 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.23.2", markers = "python_version == \"3.11\""}, {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, ] python-dateutil = ">=2.8.2" pytz = ">=2020.1" @@ -4956,18 +4956,18 @@ files = [ [[package]] name = "pydantic" -version = "2.7.2" +version = "2.7.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.7.2-py3-none-any.whl", hash = "sha256:834ab954175f94e6e68258537dc49402c4a5e9d0409b9f1b86b7e934a8372de7"}, - {file = "pydantic-2.7.2.tar.gz", hash = "sha256:71b2945998f9c9b7919a45bde9a50397b289937d215ae141c1d0903ba7149fd7"}, + {file = "pydantic-2.7.3-py3-none-any.whl", hash = "sha256:ea91b002777bf643bb20dd717c028ec43216b24a6001a280f83877fd2655d0b4"}, + {file = "pydantic-2.7.3.tar.gz", hash = "sha256:c46c76a40bb1296728d7a8b99aa73dd70a48c3510111ff290034f860c99c419e"}, ] [package.dependencies] annotated-types = ">=0.4.0" -pydantic-core = "2.18.3" +pydantic-core = "2.18.4" typing-extensions = ">=4.6.1" [package.extras] @@ -4975,90 +4975,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.18.3" +version = "2.18.4" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.18.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:744697428fcdec6be5670460b578161d1ffe34743a5c15656be7ea82b008197c"}, - {file = "pydantic_core-2.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b40c05ced1ba4218b14986fe6f283d22e1ae2ff4c8e28881a70fb81fbfcda7"}, - {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a9a75622357076efb6b311983ff190fbfb3c12fc3a853122b34d3d358126c"}, - {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2e253af04ceaebde8eb201eb3f3e3e7e390f2d275a88300d6a1959d710539e2"}, - {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:855ec66589c68aa367d989da5c4755bb74ee92ccad4fdb6af942c3612c067e34"}, - {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d3e42bb54e7e9d72c13ce112e02eb1b3b55681ee948d748842171201a03a98a"}, - {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6ac9ffccc9d2e69d9fba841441d4259cb668ac180e51b30d3632cd7abca2b9b"}, - {file = "pydantic_core-2.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c56eca1686539fa0c9bda992e7bd6a37583f20083c37590413381acfc5f192d6"}, - {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:17954d784bf8abfc0ec2a633108207ebc4fa2df1a0e4c0c3ccbaa9bb01d2c426"}, - {file = "pydantic_core-2.18.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:98ed737567d8f2ecd54f7c8d4f8572ca7c7921ede93a2e52939416170d357812"}, - {file = "pydantic_core-2.18.3-cp310-none-win32.whl", hash = "sha256:9f9e04afebd3ed8c15d67a564ed0a34b54e52136c6d40d14c5547b238390e779"}, - {file = "pydantic_core-2.18.3-cp310-none-win_amd64.whl", hash = "sha256:45e4ffbae34f7ae30d0047697e724e534a7ec0a82ef9994b7913a412c21462a0"}, - {file = "pydantic_core-2.18.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b9ebe8231726c49518b16b237b9fe0d7d361dd221302af511a83d4ada01183ab"}, - {file = "pydantic_core-2.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b8e20e15d18bf7dbb453be78a2d858f946f5cdf06c5072453dace00ab652e2b2"}, - {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0d9ff283cd3459fa0bf9b0256a2b6f01ac1ff9ffb034e24457b9035f75587cb"}, - {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f7ef5f0ebb77ba24c9970da18b771711edc5feaf00c10b18461e0f5f5949231"}, - {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73038d66614d2e5cde30435b5afdced2b473b4c77d4ca3a8624dd3e41a9c19be"}, - {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6afd5c867a74c4d314c557b5ea9520183fadfbd1df4c2d6e09fd0d990ce412cd"}, - {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd7df92f28d351bb9f12470f4c533cf03d1b52ec5a6e5c58c65b183055a60106"}, - {file = "pydantic_core-2.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80aea0ffeb1049336043d07799eace1c9602519fb3192916ff525b0287b2b1e4"}, - {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaee40f25bba38132e655ffa3d1998a6d576ba7cf81deff8bfa189fb43fd2bbe"}, - {file = "pydantic_core-2.18.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9128089da8f4fe73f7a91973895ebf2502539d627891a14034e45fb9e707e26d"}, - {file = "pydantic_core-2.18.3-cp311-none-win32.whl", hash = "sha256:fec02527e1e03257aa25b1a4dcbe697b40a22f1229f5d026503e8b7ff6d2eda7"}, - {file = "pydantic_core-2.18.3-cp311-none-win_amd64.whl", hash = "sha256:58ff8631dbab6c7c982e6425da8347108449321f61fe427c52ddfadd66642af7"}, - {file = "pydantic_core-2.18.3-cp311-none-win_arm64.whl", hash = "sha256:3fc1c7f67f34c6c2ef9c213e0f2a351797cda98249d9ca56a70ce4ebcaba45f4"}, - {file = "pydantic_core-2.18.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f0928cde2ae416a2d1ebe6dee324709c6f73e93494d8c7aea92df99aab1fc40f"}, - {file = "pydantic_core-2.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bee9bb305a562f8b9271855afb6ce00223f545de3d68560b3c1649c7c5295e9"}, - {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e862823be114387257dacbfa7d78547165a85d7add33b446ca4f4fae92c7ff5c"}, - {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a36f78674cbddc165abab0df961b5f96b14461d05feec5e1f78da58808b97e7"}, - {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba905d184f62e7ddbb7a5a751d8a5c805463511c7b08d1aca4a3e8c11f2e5048"}, - {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fdd362f6a586e681ff86550b2379e532fee63c52def1c666887956748eaa326"}, - {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b214b7ee3bd3b865e963dbed0f8bc5375f49449d70e8d407b567af3222aae4"}, - {file = "pydantic_core-2.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:691018785779766127f531674fa82bb368df5b36b461622b12e176c18e119022"}, - {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:60e4c625e6f7155d7d0dcac151edf5858102bc61bf959d04469ca6ee4e8381bd"}, - {file = "pydantic_core-2.18.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4e651e47d981c1b701dcc74ab8fec5a60a5b004650416b4abbef13db23bc7be"}, - {file = "pydantic_core-2.18.3-cp312-none-win32.whl", hash = "sha256:ffecbb5edb7f5ffae13599aec33b735e9e4c7676ca1633c60f2c606beb17efc5"}, - {file = "pydantic_core-2.18.3-cp312-none-win_amd64.whl", hash = "sha256:2c8333f6e934733483c7eddffdb094c143b9463d2af7e6bd85ebcb2d4a1b82c6"}, - {file = "pydantic_core-2.18.3-cp312-none-win_arm64.whl", hash = "sha256:7a20dded653e516a4655f4c98e97ccafb13753987434fe7cf044aa25f5b7d417"}, - {file = "pydantic_core-2.18.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:eecf63195be644b0396f972c82598cd15693550f0ff236dcf7ab92e2eb6d3522"}, - {file = "pydantic_core-2.18.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c44efdd3b6125419c28821590d7ec891c9cb0dff33a7a78d9d5c8b6f66b9702"}, - {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e59fca51ffbdd1638b3856779342ed69bcecb8484c1d4b8bdb237d0eb5a45e2"}, - {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:70cf099197d6b98953468461d753563b28e73cf1eade2ffe069675d2657ed1d5"}, - {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:63081a49dddc6124754b32a3774331467bfc3d2bd5ff8f10df36a95602560361"}, - {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:370059b7883485c9edb9655355ff46d912f4b03b009d929220d9294c7fd9fd60"}, - {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a64faeedfd8254f05f5cf6fc755023a7e1606af3959cfc1a9285744cc711044"}, - {file = "pydantic_core-2.18.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19d2e725de0f90d8671f89e420d36c3dd97639b98145e42fcc0e1f6d492a46dc"}, - {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:67bc078025d70ec5aefe6200ef094576c9d86bd36982df1301c758a9fff7d7f4"}, - {file = "pydantic_core-2.18.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:adf952c3f4100e203cbaf8e0c907c835d3e28f9041474e52b651761dc248a3c0"}, - {file = "pydantic_core-2.18.3-cp38-none-win32.whl", hash = "sha256:9a46795b1f3beb167eaee91736d5d17ac3a994bf2215a996aed825a45f897558"}, - {file = "pydantic_core-2.18.3-cp38-none-win_amd64.whl", hash = "sha256:200ad4e3133cb99ed82342a101a5abf3d924722e71cd581cc113fe828f727fbc"}, - {file = "pydantic_core-2.18.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:304378b7bf92206036c8ddd83a2ba7b7d1a5b425acafff637172a3aa72ad7083"}, - {file = "pydantic_core-2.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c826870b277143e701c9ccf34ebc33ddb4d072612683a044e7cce2d52f6c3fef"}, - {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e201935d282707394f3668380e41ccf25b5794d1b131cdd96b07f615a33ca4b1"}, - {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5560dda746c44b48bf82b3d191d74fe8efc5686a9ef18e69bdabccbbb9ad9442"}, - {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b32c2a1f8032570842257e4c19288eba9a2bba4712af542327de9a1204faff8"}, - {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:929c24e9dea3990bc8bcd27c5f2d3916c0c86f5511d2caa69e0d5290115344a9"}, - {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1a8376fef60790152564b0eab376b3e23dd6e54f29d84aad46f7b264ecca943"}, - {file = "pydantic_core-2.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dccf3ef1400390ddd1fb55bf0632209d39140552d068ee5ac45553b556780e06"}, - {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41dbdcb0c7252b58fa931fec47937edb422c9cb22528f41cb8963665c372caf6"}, - {file = "pydantic_core-2.18.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:666e45cf071669fde468886654742fa10b0e74cd0fa0430a46ba6056b24fb0af"}, - {file = "pydantic_core-2.18.3-cp39-none-win32.whl", hash = "sha256:f9c08cabff68704a1b4667d33f534d544b8a07b8e5d039c37067fceb18789e78"}, - {file = "pydantic_core-2.18.3-cp39-none-win_amd64.whl", hash = "sha256:4afa5f5973e8572b5c0dcb4e2d4fda7890e7cd63329bd5cc3263a25c92ef0026"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:77319771a026f7c7d29c6ebc623de889e9563b7087911b46fd06c044a12aa5e9"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:df11fa992e9f576473038510d66dd305bcd51d7dd508c163a8c8fe148454e059"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d531076bdfb65af593326ffd567e6ab3da145020dafb9187a1d131064a55f97c"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d33ce258e4e6e6038f2b9e8b8a631d17d017567db43483314993b3ca345dcbbb"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1f9cd7f5635b719939019be9bda47ecb56e165e51dd26c9a217a433e3d0d59a9"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cd4a032bb65cc132cae1fe3e52877daecc2097965cd3914e44fbd12b00dae7c5"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f2718430098bcdf60402136c845e4126a189959d103900ebabb6774a5d9fdb"}, - {file = "pydantic_core-2.18.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c0037a92cf0c580ed14e10953cdd26528e8796307bb8bb312dc65f71547df04d"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b95a0972fac2b1ff3c94629fc9081b16371dad870959f1408cc33b2f78ad347a"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a62e437d687cc148381bdd5f51e3e81f5b20a735c55f690c5be94e05da2b0d5c"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b367a73a414bbb08507da102dc2cde0fa7afe57d09b3240ce82a16d608a7679c"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ecce4b2360aa3f008da3327d652e74a0e743908eac306198b47e1c58b03dd2b"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd4435b8d83f0c9561a2a9585b1de78f1abb17cb0cef5f39bf6a4b47d19bafe3"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:616221a6d473c5b9aa83fa8982745441f6a4a62a66436be9445c65f241b86c94"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7e6382ce89a92bc1d0c0c5edd51e931432202b9080dc921d8d003e616402efd1"}, - {file = "pydantic_core-2.18.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff58f379345603d940e461eae474b6bbb6dab66ed9a851ecd3cb3709bf4dcf6a"}, - {file = "pydantic_core-2.18.3.tar.gz", hash = "sha256:432e999088d85c8f36b9a3f769a8e2b57aabd817bbb729a90d1fe7f18f6f1f39"}, + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f76d0ad001edd426b92233d45c746fd08f467d56100fd8f30e9ace4b005266e4"}, + {file = "pydantic_core-2.18.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:59ff3e89f4eaf14050c8022011862df275b552caef8082e37b542b066ce1ff26"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a55b5b16c839df1070bc113c1f7f94a0af4433fcfa1b41799ce7606e5c79ce0a"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d0dcc59664fcb8974b356fe0a18a672d6d7cf9f54746c05f43275fc48636851"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8951eee36c57cd128f779e641e21eb40bc5073eb28b2d23f33eb0ef14ffb3f5d"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4701b19f7e3a06ea655513f7938de6f108123bf7c86bbebb1196eb9bd35cf724"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00a3f196329e08e43d99b79b286d60ce46bed10f2280d25a1718399457e06be"}, + {file = "pydantic_core-2.18.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97736815b9cc893b2b7f663628e63f436018b75f44854c8027040e05230eeddb"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6891a2ae0e8692679c07728819b6e2b822fb30ca7445f67bbf6509b25a96332c"}, + {file = "pydantic_core-2.18.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bc4ff9805858bd54d1a20efff925ccd89c9d2e7cf4986144b30802bf78091c3e"}, + {file = "pydantic_core-2.18.4-cp310-none-win32.whl", hash = "sha256:1b4de2e51bbcb61fdebd0ab86ef28062704f62c82bbf4addc4e37fa4b00b7cbc"}, + {file = "pydantic_core-2.18.4-cp310-none-win_amd64.whl", hash = "sha256:6a750aec7bf431517a9fd78cb93c97b9b0c496090fee84a47a0d23668976b4b0"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:942ba11e7dfb66dc70f9ae66b33452f51ac7bb90676da39a7345e99ffb55402d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b2ebef0e0b4454320274f5e83a41844c63438fdc874ea40a8b5b4ecb7693f1c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a642295cd0c8df1b86fc3dced1d067874c353a188dc8e0f744626d49e9aa51c4"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f09baa656c904807e832cf9cce799c6460c450c4ad80803517032da0cd062e2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98906207f29bc2c459ff64fa007afd10a8c8ac080f7e4d5beff4c97086a3dabd"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19894b95aacfa98e7cb093cd7881a0c76f55731efad31073db4521e2b6ff5b7d"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fbbdc827fe5e42e4d196c746b890b3d72876bdbf160b0eafe9f0334525119c8"}, + {file = "pydantic_core-2.18.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f85d05aa0918283cf29a30b547b4df2fbb56b45b135f9e35b6807cb28bc47951"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e85637bc8fe81ddb73fda9e56bab24560bdddfa98aa64f87aaa4e4b6730c23d2"}, + {file = "pydantic_core-2.18.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2f5966897e5461f818e136b8451d0551a2e77259eb0f73a837027b47dc95dab9"}, + {file = "pydantic_core-2.18.4-cp311-none-win32.whl", hash = "sha256:44c7486a4228413c317952e9d89598bcdfb06399735e49e0f8df643e1ccd0558"}, + {file = "pydantic_core-2.18.4-cp311-none-win_amd64.whl", hash = "sha256:8a7164fe2005d03c64fd3b85649891cd4953a8de53107940bf272500ba8a788b"}, + {file = "pydantic_core-2.18.4-cp311-none-win_arm64.whl", hash = "sha256:4e99bc050fe65c450344421017f98298a97cefc18c53bb2f7b3531eb39bc7805"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6f5c4d41b2771c730ea1c34e458e781b18cc668d194958e0112455fff4e402b2"}, + {file = "pydantic_core-2.18.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fdf2156aa3d017fddf8aea5adfba9f777db1d6022d392b682d2a8329e087cef"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4748321b5078216070b151d5271ef3e7cc905ab170bbfd27d5c83ee3ec436695"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847a35c4d58721c5dc3dba599878ebbdfd96784f3fb8bb2c356e123bdcd73f34"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c40d4eaad41f78e3bbda31b89edc46a3f3dc6e171bf0ecf097ff7a0ffff7cb1"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:21a5e440dbe315ab9825fcd459b8814bb92b27c974cbc23c3e8baa2b76890077"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01dd777215e2aa86dfd664daed5957704b769e726626393438f9c87690ce78c3"}, + {file = "pydantic_core-2.18.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4b06beb3b3f1479d32befd1f3079cc47b34fa2da62457cdf6c963393340b56e9"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:564d7922e4b13a16b98772441879fcdcbe82ff50daa622d681dd682175ea918c"}, + {file = "pydantic_core-2.18.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0eb2a4f660fcd8e2b1c90ad566db2b98d7f3f4717c64fe0a83e0adb39766d5b8"}, + {file = "pydantic_core-2.18.4-cp312-none-win32.whl", hash = "sha256:8b8bab4c97248095ae0c4455b5a1cd1cdd96e4e4769306ab19dda135ea4cdb07"}, + {file = "pydantic_core-2.18.4-cp312-none-win_amd64.whl", hash = "sha256:14601cdb733d741b8958224030e2bfe21a4a881fb3dd6fbb21f071cabd48fa0a"}, + {file = "pydantic_core-2.18.4-cp312-none-win_arm64.whl", hash = "sha256:c1322d7dd74713dcc157a2b7898a564ab091ca6c58302d5c7b4c07296e3fd00f"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:823be1deb01793da05ecb0484d6c9e20baebb39bd42b5d72636ae9cf8350dbd2"}, + {file = "pydantic_core-2.18.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebef0dd9bf9b812bf75bda96743f2a6c5734a02092ae7f721c048d156d5fabae"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae1d6df168efb88d7d522664693607b80b4080be6750c913eefb77e34c12c71a"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9899c94762343f2cc2fc64c13e7cae4c3cc65cdfc87dd810a31654c9b7358cc"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99457f184ad90235cfe8461c4d70ab7dd2680e28821c29eca00252ba90308c78"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18f469a3d2a2fdafe99296a87e8a4c37748b5080a26b806a707f25a902c040a8"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cdf28938ac6b8b49ae5e92f2735056a7ba99c9b110a474473fd71185c1af5d"}, + {file = "pydantic_core-2.18.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:938cb21650855054dc54dfd9120a851c974f95450f00683399006aa6e8abb057"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:44cd83ab6a51da80fb5adbd9560e26018e2ac7826f9626bc06ca3dc074cd198b"}, + {file = "pydantic_core-2.18.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:972658f4a72d02b8abfa2581d92d59f59897d2e9f7e708fdabe922f9087773af"}, + {file = "pydantic_core-2.18.4-cp38-none-win32.whl", hash = "sha256:1d886dc848e60cb7666f771e406acae54ab279b9f1e4143babc9c2258213daa2"}, + {file = "pydantic_core-2.18.4-cp38-none-win_amd64.whl", hash = "sha256:bb4462bd43c2460774914b8525f79b00f8f407c945d50881568f294c1d9b4443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:44a688331d4a4e2129140a8118479443bd6f1905231138971372fcde37e43528"}, + {file = "pydantic_core-2.18.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a2fdd81edd64342c85ac7cf2753ccae0b79bf2dfa063785503cb85a7d3593223"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86110d7e1907ab36691f80b33eb2da87d780f4739ae773e5fc83fb272f88825f"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46387e38bd641b3ee5ce247563b60c5ca098da9c56c75c157a05eaa0933ed154"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:123c3cec203e3f5ac7b000bd82235f1a3eced8665b63d18be751f115588fea30"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dc1803ac5c32ec324c5261c7209e8f8ce88e83254c4e1aebdc8b0a39f9ddb443"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53db086f9f6ab2b4061958d9c276d1dbe3690e8dd727d6abf2321d6cce37fa94"}, + {file = "pydantic_core-2.18.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:abc267fa9837245cc28ea6929f19fa335f3dc330a35d2e45509b6566dc18be23"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0d829524aaefdebccb869eed855e2d04c21d2d7479b6cada7ace5448416597b"}, + {file = "pydantic_core-2.18.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:509daade3b8649f80d4e5ff21aa5673e4ebe58590b25fe42fac5f0f52c6f034a"}, + {file = "pydantic_core-2.18.4-cp39-none-win32.whl", hash = "sha256:ca26a1e73c48cfc54c4a76ff78df3727b9d9f4ccc8dbee4ae3f73306a591676d"}, + {file = "pydantic_core-2.18.4-cp39-none-win_amd64.whl", hash = "sha256:c67598100338d5d985db1b3d21f3619ef392e185e71b8d52bceacc4a7771ea7e"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:574d92eac874f7f4db0ca653514d823a0d22e2354359d0759e3f6a406db5d55d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1f4d26ceb5eb9eed4af91bebeae4b06c3fb28966ca3a8fb765208cf6b51102ab"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77450e6d20016ec41f43ca4a6c63e9fdde03f0ae3fe90e7c27bdbeaece8b1ed4"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d323a01da91851a4f17bf592faf46149c9169d68430b3146dcba2bb5e5719abc"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43d447dd2ae072a0065389092a231283f62d960030ecd27565672bd40746c507"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:578e24f761f3b425834f297b9935e1ce2e30f51400964ce4801002435a1b41ef"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:81b5efb2f126454586d0f40c4d834010979cb80785173d1586df845a632e4e6d"}, + {file = "pydantic_core-2.18.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ab86ce7c8f9bea87b9d12c7f0af71102acbf5ecbc66c17796cff45dae54ef9a5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:90afc12421df2b1b4dcc975f814e21bc1754640d502a2fbcc6d41e77af5ec312"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:51991a89639a912c17bef4b45c87bd83593aee0437d8102556af4885811d59f5"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:293afe532740370aba8c060882f7d26cfd00c94cae32fd2e212a3a6e3b7bc15e"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b48ece5bde2e768197a2d0f6e925f9d7e3e826f0ad2271120f8144a9db18d5c8"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eae237477a873ab46e8dd748e515c72c0c804fb380fbe6c85533c7de51f23a8f"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:834b5230b5dfc0c1ec37b2fda433b271cbbc0e507560b5d1588e2cc1148cf1ce"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e858ac0a25074ba4bce653f9b5d0a85b7456eaddadc0ce82d3878c22489fa4ee"}, + {file = "pydantic_core-2.18.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2fd41f6eff4c20778d717af1cc50eca52f5afe7805ee530a4fbd0bae284f16e9"}, + {file = "pydantic_core-2.18.4.tar.gz", hash = "sha256:ec3beeada09ff865c344ff3bc2f427f5e6c26401cc6113d77e372c3fdac73864"}, ] [package.dependencies] @@ -7601,6 +7601,22 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[[package]] +name = "xlrd" +version = "2.0.1" +description = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd"}, + {file = "xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"}, +] + +[package.extras] +build = ["twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + [[package]] name = "xxhash" version = "3.4.1" @@ -7839,4 +7855,4 @@ test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-it [metadata] lock-version = "2.0" python-versions = ">=3.10.0,<3.12" -content-hash = "5c891b42861023d77d87bf4027b2b3e53fe7eaad67d81c893f7f5be7e45a8e69" +content-hash = "efc14eedd786e12d84883e0b2dd2319a4b538a1a88891d995cd4c3fde14da2c3" diff --git a/pyproject.toml b/pyproject.toml index 5398cebc..aa7ce73b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ openinference-instrumentation = "^0.1.7" llama-index-llms-huggingface = "^0.2.0" pytest-asyncio = "^0.23.7" pytest-cov = "^5.0.0" +xlrd = "^2.0.1" [tool.poetry.scripts] pai_rag = "pai_rag.main:main" diff --git a/src/pai_rag/docs/tabular_doc.md b/src/pai_rag/docs/tabular_doc.md new file mode 100644 index 00000000..8791cf64 --- /dev/null +++ b/src/pai_rag/docs/tabular_doc.md @@ -0,0 +1,76 @@ +# Tabular processing with PAI-RAG + +## PaiCSVReader + +PaiCSVReader(concat_rows=True, row_joiner="\n", csv_config={}) + +### Parameters: + +**concat_rows:** _bool, default=True._ +Whether to concatenate rows into one document. + +**row_joiner:** _str, default="\n"._ +The separator used to join rows. + +**header:** _None or int, list of int, default 0._ +row (0-indexed) to use for the column labels of the parsed DataFrame. If a list of integers is passed those row +positions will be combined into a MultiIndex. Use None if there is no header. + +### Functions: + +load_data(file: Path, extra_info: Optional[Dict] = None, fs: Optional[AbstractFileSystem] = None) + +## PaiPandasCSVReader + +PaiPandasCSVReader(concat_rows=True, row_joiner="\n", pandas_config={}) + +### Parameters: + +**concat_rows:** _bool, default=True._ +Whether to concatenate rows into one document. + +**row_joiner:** _str, default="\n"._ +The separator used to join rows. + +**pandas_config:** _dict, default={}._ +The configuration of pandas.read_csv. +Refer to https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html for more information. +Set to empty dict by default, this means pandas will try to figure out the separators, table head, etc. on its own. + +#### one important parameter: + +**header:** _None or int, list of int, default 0._ +Row (0-indexed) to use for the column labels of the parsed DataFrame. If a list of integers is passed those row +positions will be combined into a MultiIndex. Use None if there is no header. + +### Functions: + +load_data(file: Path, extra_info: Optional[Dict] = None, fs: Optional[AbstractFileSystem] = None) + +## PaiPandasExcelReader + +PaiPandasExcelReader(concat_rows=True, row_joiner="\n", pandas_config={}) + +### Parameters: + +**concat_rows:** _bool, default=True._ +Whether to concatenate rows into one document. + +**row_joiner:** _str, default="\n"._ +The separator used to join rows. + +**pandas_config:** _dict, default={}._ +The configuration of pandas.read_csv. +Refer to https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html for more information. +Set to empty dict by default, this means pandas will try to figure out the separators, table head, etc. on its own. + +#### one important parameter: + +**header:** _None or int, list of int, default 0._ +Row (0-indexed) to use for the column labels of the parsed DataFrame. If a list of integers is passed those row +positions will be combined into a MultiIndex. Use None if there is no header. + +### Functions: + +load_data(file: Path, extra_info: Optional[Dict] = None, fs: Optional[AbstractFileSystem] = None) +only process the first sheet diff --git a/src/pai_rag/integrations/readers/pai_csv_reader.py b/src/pai_rag/integrations/readers/pai_csv_reader.py new file mode 100644 index 00000000..3d1bde60 --- /dev/null +++ b/src/pai_rag/integrations/readers/pai_csv_reader.py @@ -0,0 +1,151 @@ +"""Tabular parser-CSV parser. + +Contains parsers for tabular data files. + +""" + +from pathlib import Path +from typing import Any, Dict, List, Optional +from fsspec import AbstractFileSystem + +import pandas as pd +from llama_index.core.readers.base import BaseReader +from llama_index.core.schema import Document + + +class PaiCSVReader(BaseReader): + """CSV parser. + + Args: + concat_rows (bool): whether to concatenate all rows into one document. + If set to False, a Document will be created for each row. + True by default. + header (object): None or int, list of int, default 0. + Row (0-indexed) to use for the column labels of the parsed DataFrame. If a list of integers is passed those row + positions will be combined into a MultiIndex. Use None if there is no header. + + """ + + def __init__( + self, *args: Any, concat_rows: bool = True, header: object = 0, **kwargs: Any + ) -> None: + """Init params.""" + super().__init__(*args, **kwargs) + self._concat_rows = concat_rows + self._header = header + + def load_data( + self, file: Path, extra_info: Optional[Dict] = None + ) -> List[Document]: + """Parse csv file. + + Returns: + Union[str, List[str]]: a string or a List of strings. + + """ + try: + import csv + except ImportError: + raise ImportError("csv module is required to read CSV files.") + text_list = [] + headers = [] + data_lines = [] + data_line_start_index = 1 + if isinstance(self._header, list): + data_line_start_index = max(self._header) + 1 + elif isinstance(self._header, int): + data_line_start_index = self._header + 1 + self._header = [self._header] + + with open(file) as fp: + csv_reader = csv.reader(fp) + + if self._header is None: + for row in csv_reader: + text_list.append(", ".join(row)) + else: + for i, row in enumerate(csv_reader): + if i in self._header: + headers.append(row) + elif i >= data_line_start_index: + data_lines.append(row) + headers = [tuple(group) for group in zip(*headers)] + for line in data_lines: + if len(line) == len(headers): + data_entry = str(dict(zip(headers, line))) + text_list.append(data_entry) + + metadata = {"filename": file.name, "extension": file.suffix} + if extra_info: + metadata = {**metadata, **extra_info} + + if self._concat_rows: + return [Document(text="\n".join(text_list), metadata=metadata)] + else: + return [Document(text=text, metadata=metadata) for text in text_list] + + +class PaiPandasCSVReader(BaseReader): + r"""Pandas-based CSV parser. + + Parses CSVs using the separator detection from Pandas `read_csv`function. + If special parameters are required, use the `pandas_config` dict. + + Args: + concat_rows (bool): whether to concatenate all rows into one document. + If set to False, a Document will be created for each row. + True by default. + + row_joiner (str): Separator to use for joining each row. + Only used when `concat_rows=True`. + Set to "\n" by default. + + pandas_config (dict): Options for the `pandas.read_csv` function call. + Refer to https://pandas.pydata.org/docs/reference/api/pandas.read_csv.html + for more information. + Set to empty dict by default, this means pandas will try to figure + out the separators, table head, etc. on its own. + + """ + + def __init__( + self, + *args: Any, + concat_rows: bool = True, + row_joiner: str = "\n", + pandas_config: dict = {}, + **kwargs: Any + ) -> None: + """Init params.""" + super().__init__(*args, **kwargs) + self._concat_rows = concat_rows + self._row_joiner = row_joiner + self._pandas_config = pandas_config + + def load_data( + self, + file: Path, + extra_info: Optional[Dict] = None, + fs: Optional[AbstractFileSystem] = None, + ) -> List[Document]: + """Parse csv file.""" + if fs: + with fs.open(file) as f: + df = pd.read_csv(f, **self._pandas_config) + else: + df = pd.read_csv(file, **self._pandas_config) + + text_list = df.apply( + lambda row: str(dict(zip(df.columns, row.astype(str)))), axis=1 + ).tolist() + + if self._concat_rows: + return [ + Document( + text=(self._row_joiner).join(text_list), metadata=extra_info or {} + ) + ] + else: + return [ + Document(text=text, metadata=extra_info or {}) for text in text_list + ] diff --git a/src/pai_rag/integrations/readers/pai_excel_reader.py b/src/pai_rag/integrations/readers/pai_excel_reader.py new file mode 100644 index 00000000..c300985a --- /dev/null +++ b/src/pai_rag/integrations/readers/pai_excel_reader.py @@ -0,0 +1,121 @@ +"""Tabular parser-Excel parser. + +Contains parsers for tabular data files. + +""" + +from pathlib import Path +from typing import Any, Dict, List, Optional +from fsspec import AbstractFileSystem +from openpyxl import load_workbook + +import pandas as pd +from llama_index.core.readers.base import BaseReader +from llama_index.core.schema import Document + + +class PaiPandasExcelReader(BaseReader): + r"""Pandas-based Excel parser. + + + Args: + concat_rows (bool): whether to concatenate all rows into one document. + If set to False, a Document will be created for each row. + True by default. + + row_joiner (str): Separator to use for joining each row. + Only used when `concat_rows=True`. + Set to "\n" by default. + + pandas_config (dict): Options for the `pandas.read_excel` function call. + Refer to https://pandas.pydata.org/docs/reference/api/pandas.read_excel.html + for more information. + Set to empty dict by default, this means pandas will try to figure + out the separators, table head, etc. on its own. + + """ + + def __init__( + self, + *args: Any, + concat_rows: bool = True, + row_joiner: str = "\n", + pandas_config: dict = {}, + **kwargs: Any + ) -> None: + """Init params.""" + super().__init__(*args, **kwargs) + self._concat_rows = concat_rows + self._row_joiner = row_joiner + self._pandas_config = pandas_config + + def read_xlsx( + self, + file: Path, + fs: Optional[AbstractFileSystem] = None, + ): + """Parse Excel file。""" + if fs: + with fs.open(file) as f: + excel = pd.ExcelFile(load_workbook(f), engine="openpyxl") + else: + excel = pd.ExcelFile(load_workbook(file), engine="openpyxl") + sheet_name = excel.sheet_names[0] + sheet = excel.book[sheet_name] + df = excel.parse(sheet_name, **self._pandas_config) + + header_max = 0 + if ( + "header" in self._pandas_config + and self._pandas_config["header"] is not None + and isinstance(self._pandas_config["header"], list) + ): + header_max = max(self._pandas_config["header"]) + elif ( + "header" in self._pandas_config + and self._pandas_config["header"] is not None + and isinstance(self._pandas_config["header"], int) + ): + header_max = self._pandas_config["header"] + + for item in sheet.merged_cells: + top_col, top_row, bottom_col, bottom_row = item.bounds + base_value = item.start_cell.value + # Convert 1-based index to 0-based index + top_row -= 1 + top_col -= 1 + # Since the previous lines are set as headers, the coordinates need to be adjusted here. + if ( + "header" in self._pandas_config + and self._pandas_config["header"] is not None + ) or "header" not in self._pandas_config: + top_row -= header_max + 1 + bottom_row -= header_max + 1 + + df.iloc[top_row:bottom_row, top_col:bottom_col] = base_value + return df + + def load_data( + self, + file: Path, + extra_info: Optional[Dict] = None, + fs: Optional[AbstractFileSystem] = None, + ) -> List[Document]: + """Parse Excel file. only process the first sheet""" + + df = self.read_xlsx(file, fs) + + text_list = df.apply( + lambda row: str(dict(zip(df.columns, row.astype(str)))), axis=1 + ).tolist() + + if self._concat_rows: + return [ + Document( + text=(self._row_joiner).join(text_list), metadata=extra_info or {} + ) + ] + else: + return [ + Document(text=text, metadata=extra_info or {}) for text in text_list + ] diff --git a/src/pai_rag/modules/datareader/datareader_factory.py b/src/pai_rag/modules/datareader/datareader_factory.py index abb46105..8c0b0c60 100644 --- a/src/pai_rag/modules/datareader/datareader_factory.py +++ b/src/pai_rag/modules/datareader/datareader_factory.py @@ -4,6 +4,8 @@ from pai_rag.integrations.readers.pai_pdf_reader import PaiPDFReader from pai_rag.integrations.readers.llama_parse_reader import LlamaParseDirectoryReader from pai_rag.integrations.readers.html.html_reader import HtmlReader +from pai_rag.integrations.readers.pai_csv_reader import PaiPandasCSVReader +from pai_rag.integrations.readers.pai_excel_reader import PaiPandasExcelReader from llama_index.readers.database import DatabaseReader from llama_index.core import SimpleDirectoryReader import logging @@ -25,6 +27,15 @@ def _create_new_instance(self, new_params: Dict[str, Any]): enable_image_ocr=self.reader_config.get("enable_image_ocr", False), model_dir=self.reader_config.get("easyocr_model_dir", None), ), + ".csv": PaiPandasCSVReader( + concat_rows=self.reader_config.get("concat_rows", False), + ), + ".xlsx": PaiPandasExcelReader( + concat_rows=self.reader_config.get("concat_rows", False), + ), + ".xls": PaiPandasExcelReader( + concat_rows=self.reader_config.get("concat_rows", False), + ), } return self diff --git a/tests/data_readers/test_csv_reader.py b/tests/data_readers/test_csv_reader.py new file mode 100644 index 00000000..f3de5b14 --- /dev/null +++ b/tests/data_readers/test_csv_reader.py @@ -0,0 +1,48 @@ +import os +from pathlib import Path +from pai_rag.core.rag_configuration import RagConfiguration +from pai_rag.modules.module_registry import module_registry +from pai_rag.integrations.readers.pai_csv_reader import PaiCSVReader, PaiPandasCSVReader +from llama_index.core import SimpleDirectoryReader + +BASE_DIR = Path(__file__).parent.parent.parent + + +def test_csv_reader(): + config_file = os.path.join(BASE_DIR, "src/pai_rag/config/settings.toml") + config = RagConfiguration.from_file(config_file).get_value() + module_registry.init_modules(config) + reader_config = config["data_reader"] + directory_reader = SimpleDirectoryReader( + input_dir="tests/testdata/data/csv_data", + file_extractor={ + ".csv": PaiCSVReader( + concat_rows=reader_config.get("concat_rows", False), + header=[0, 1], + ) + }, + ) + documents = directory_reader.load_data() + for doc in documents: + print(doc) + assert len(documents) == 7 + + +def test_pandas_csv_reader(): + config_file = os.path.join(BASE_DIR, "src/pai_rag/config/settings.toml") + config = RagConfiguration.from_file(config_file).get_value() + module_registry.init_modules(config) + reader_config = config["data_reader"] + directory_reader = SimpleDirectoryReader( + input_dir="tests/testdata/data/csv_data", + file_extractor={ + ".csv": PaiPandasCSVReader( + concat_rows=reader_config.get("concat_rows", False), + pandas_config={"header": [0, 1]}, + ) + }, + ) + documents = directory_reader.load_data() + for doc in documents: + print(doc) + assert len(documents) == 7 diff --git a/tests/data_readers/test_excel_reader.py b/tests/data_readers/test_excel_reader.py new file mode 100644 index 00000000..c98aad42 --- /dev/null +++ b/tests/data_readers/test_excel_reader.py @@ -0,0 +1,32 @@ +import os +from pathlib import Path +from pai_rag.core.rag_configuration import RagConfiguration +from pai_rag.modules.module_registry import module_registry +from pai_rag.integrations.readers.pai_excel_reader import PaiPandasExcelReader +from llama_index.core import SimpleDirectoryReader + +BASE_DIR = Path(__file__).parent.parent.parent + + +def test_pandas_excel_reader(): + config_file = os.path.join(BASE_DIR, "src/pai_rag/config/settings.toml") + config = RagConfiguration.from_file(config_file).get_value() + module_registry.init_modules(config) + reader_config = config["data_reader"] + directory_reader = SimpleDirectoryReader( + input_dir="tests/testdata/data/excel_data", + file_extractor={ + ".xlsx": PaiPandasExcelReader( + concat_rows=reader_config.get("concat_rows", False), + pandas_config={"header": [0, 1]}, + ), + ".xls": PaiPandasExcelReader( + concat_rows=reader_config.get("concat_rows", False), + pandas_config={"header": [0, 1]}, + ), + }, + ) + documents = directory_reader.load_data() + for doc in documents: + print(doc) + assert len(documents) == 7 diff --git "a/tests/testdata/data/csv_data/30\345\244\251\347\225\231\345\255\230\347\216\207_csv_two_header.csv" "b/tests/testdata/data/csv_data/30\345\244\251\347\225\231\345\255\230\347\216\207_csv_two_header.csv" new file mode 100644 index 00000000..b0585488 --- /dev/null +++ "b/tests/testdata/data/csv_data/30\345\244\251\347\225\231\345\255\230\347\216\207_csv_two_header.csv" @@ -0,0 +1,9 @@ +time,metric +date,rate +20240101,0.9375 +20240201,0.9744 +20240301,0.9767 +20240401,0.9375 +20240501,0.9091 +20240601,0.9474 +20240701,0.9667 \ No newline at end of file diff --git "a/tests/testdata/data/excel_data/30\345\244\251\347\225\231\345\255\230\347\216\207_excel_two_header.xlsx" "b/tests/testdata/data/excel_data/30\345\244\251\347\225\231\345\255\230\347\216\207_excel_two_header.xlsx" new file mode 100644 index 0000000000000000000000000000000000000000..dc548004539cff0dff3adf34dfcef10705d628c3 GIT binary patch literal 9147 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 literal 0 HcmV?d00001