From 59985538b4c67370b6aa5d9282bb1b896a2110bb Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 10:59:51 +0100 Subject: [PATCH 01/10] Add ADBC sqlite --- pixi.lock | 199 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 11 ++- 2 files changed, 208 insertions(+), 2 deletions(-) diff --git a/pixi.lock b/pixi.lock index d5fd5a3..bea4c2a 100644 --- a/pixi.lock +++ b/pixi.lock @@ -12,6 +12,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/adbc-driver-manager-1.9.0-py312h1289d80_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda @@ -54,6 +56,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda @@ -71,6 +74,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250127.1-cxx17_hbbce691_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libadbc-driver-sqlite-1.9.0-hcea63bf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-20.0.0-h1b9301b_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-20.0.0-hcb10f89_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-20.0.0-hcb10f89_8_cpu.conda @@ -176,6 +180,8 @@ environments: - pypi: ./ osx-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/adbc-driver-manager-1.9.0-py312h1eb2caf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/argon2-cffi-bindings-25.1.0-py312h80b0991_2.conda @@ -218,6 +224,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda @@ -233,6 +240,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250127.1-cxx17_h0e468a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libadbc-driver-sqlite-1.9.0-h4340512_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-20.0.0-h7601d43_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-20.0.0-hdc53af8_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-20.0.0-hdc53af8_8_cpu.conda @@ -333,6 +341,8 @@ environments: - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/adbc-driver-manager-1.9.0-py312h455b684_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/argon2-cffi-bindings-25.1.0-py312h4409184_2.conda @@ -375,6 +385,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda @@ -390,6 +401,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250127.1-cxx17_h07bc746_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libadbc-driver-sqlite-1.9.0-hdd8eb12_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-20.0.0-hd5f8272_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-20.0.0-hf07054f_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-20.0.0-hf07054f_8_cpu.conda @@ -490,6 +502,8 @@ environments: - pypi: ./ win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/adbc-driver-manager-1.9.0-py312hbb81ca0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda @@ -524,6 +538,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda @@ -539,6 +554,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250127.1-cxx17_h4eb7d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libadbc-driver-sqlite-1.9.0-h18ee088_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-20.0.0-h3e40a90_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-20.0.0-h7d8d6a5_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-20.0.0-h7d8d6a5_8_cpu.conda @@ -639,6 +655,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/adbc-driver-manager-1.9.0-py312h1289d80_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda @@ -716,6 +734,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyha191276_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda @@ -744,6 +763,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250127.1-cxx17_hbbce691_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libadbc-driver-sqlite-1.9.0-hcea63bf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-20.0.0-h1b9301b_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-20.0.0-hcb10f89_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-20.0.0-hcb10f89_8_cpu.conda @@ -907,6 +927,8 @@ environments: - pypi: ./ osx-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/adbc-driver-manager-1.9.0-py312h1eb2caf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda @@ -985,6 +1007,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda @@ -1011,6 +1034,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250127.1-cxx17_h0e468a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libadbc-driver-sqlite-1.9.0-h4340512_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-20.0.0-h7601d43_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-20.0.0-hdc53af8_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-20.0.0-hdc53af8_8_cpu.conda @@ -1168,6 +1192,8 @@ environments: - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/adbc-driver-manager-1.9.0-py312h455b684_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda @@ -1246,6 +1272,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda @@ -1272,6 +1299,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250127.1-cxx17_h07bc746_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libadbc-driver-sqlite-1.9.0-hdd8eb12_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-20.0.0-hd5f8272_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-20.0.0-hf07054f_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-20.0.0-hf07054f_8_cpu.conda @@ -1429,6 +1457,8 @@ environments: - pypi: ./ win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/adbc-driver-manager-1.9.0-py312hbb81ca0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda @@ -1498,6 +1528,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/identify-2.6.15-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh6dadd2b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyhe2676ad_0.conda @@ -1524,6 +1555,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250127.1-cxx17_h4eb7d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libadbc-driver-sqlite-1.9.0-h18ee088_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-20.0.0-h3e40a90_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-20.0.0-h7d8d6a5_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-20.0.0-h7d8d6a5_8_cpu.conda @@ -1682,6 +1714,8 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/adbc-driver-manager-1.9.0-py312h1289d80_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py312h4c3975b_2.conda @@ -1745,6 +1779,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyha191276_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda @@ -1770,6 +1805,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45-default_hbd61a6d_104.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20250127.1-cxx17_hbbce691_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libadbc-driver-sqlite-1.9.0-hcea63bf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-20.0.0-h1b9301b_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-20.0.0-hcb10f89_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-20.0.0-hcb10f89_8_cpu.conda @@ -1898,6 +1934,8 @@ environments: - pypi: ./ osx-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/adbc-driver-manager-1.9.0-py312h1eb2caf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda @@ -1962,6 +2000,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/icu-75.1-h120a0e1_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda @@ -1985,6 +2024,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.3-h37d8d59_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libabseil-20250127.1-cxx17_h0e468a2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-64/libadbc-driver-sqlite-1.9.0-h4340512_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-20.0.0-h7601d43_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-acero-20.0.0-hdc53af8_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-64/libarrow-dataset-20.0.0-hdc53af8_8_cpu.conda @@ -2107,6 +2147,8 @@ environments: - pypi: ./ osx-arm64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/adbc-driver-manager-1.9.0-py312h455b684_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda @@ -2171,6 +2213,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh5552912_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyh53cf698_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda @@ -2194,6 +2237,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20250127.1-cxx17_h07bc746_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libadbc-driver-sqlite-1.9.0-hdd8eb12_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-20.0.0-hd5f8272_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-acero-20.0.0-hf07054f_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarrow-dataset-20.0.0-hf07054f_8_cpu.conda @@ -2316,6 +2360,8 @@ environments: - pypi: ./ win-64: - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/adbc-driver-manager-1.9.0-py312hbb81ca0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/argon2-cffi-bindings-25.1.0-py312he06e257_2.conda @@ -2371,6 +2417,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.1.0-pyh6dadd2b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.8.0-pyhe2676ad_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda @@ -2394,6 +2441,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/lark-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libabseil-20250127.1-cxx17_h4eb7d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libadbc-driver-sqlite-1.9.0-h18ee088_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-20.0.0-h3e40a90_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-acero-20.0.0-h7d8d6a5_8_cpu.conda - conda: https://conda.anaconda.org/conda-forge/win-64/libarrow-dataset-20.0.0-h7d8d6a5_8_cpu.conda @@ -2538,6 +2586,89 @@ packages: purls: [] size: 8191 timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/linux-64/adbc-driver-manager-1.9.0-py312h1289d80_1.conda + sha256: cee0eb8a92563267ded63f61901922fe8e115cf0b694bfd39548fee59d25ff2f + md5: 04f547f5f1f2022bdb0a556dcc566d5a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - pyarrow >=8.0.0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/adbc-driver-manager?source=hash-mapping + size: 346251 + timestamp: 1764031540423 +- conda: https://conda.anaconda.org/conda-forge/osx-64/adbc-driver-manager-1.9.0-py312h1eb2caf_1.conda + sha256: 88fa88081d6376152383c7964340403f6a6e998212cddc87965c2e55957dcb96 + md5: 83a2664f12bfc21d7f2a21e312472935 + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - pyarrow >=8.0.0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/adbc-driver-manager?source=hash-mapping + size: 311547 + timestamp: 1764031904949 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/adbc-driver-manager-1.9.0-py312h455b684_1.conda + sha256: f4e18f38f7d1b8c569c83cd4caff30ffeb3949d99b6f3718685d47bf201c3916 + md5: 220b002cc30013d69b584b64ff82113a + depends: + - __osx >=11.0 + - libcxx >=19 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + constrains: + - pyarrow >=8.0.0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/adbc-driver-manager?source=hash-mapping + size: 298419 + timestamp: 1764031973405 +- conda: https://conda.anaconda.org/conda-forge/win-64/adbc-driver-manager-1.9.0-py312hbb81ca0_1.conda + sha256: 0613da8a43fb4f7b0b15604e2acbfcf8084bce3c2725df89b983156f525fb0bf + md5: 16a774881beceaa6f3dd41da99dec7b5 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - pyarrow >=8.0.0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/adbc-driver-manager?source=hash-mapping + size: 430584 + timestamp: 1764031648991 +- conda: https://conda.anaconda.org/conda-forge/noarch/adbc-driver-sqlite-1.9.0-pyha770c72_1.conda + sha256: 83c209bb144840e3375b2b525a4ed2dd7fc0e3c6a8da7273b122b08c29c8f567 + md5: 80c8e7a339ad9423faa0b515ec0ea0a2 + depends: + - adbc-driver-manager >=1.9.0,<2.0a0 + - importlib_resources + - libadbc-driver-sqlite >=1.9.0,<1.9.1.0a0 + - python >=3.10 + constrains: + - pyarrow >=8.0.0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/adbc-driver-sqlite?source=hash-mapping + size: 24709 + timestamp: 1764032240842 - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.0-pyhcf101f3_0.conda sha256: 830fc81970cd9d19869909b9b16d241f4d557e4f201a1030aa6ed87c6aa8b930 md5: 9958d4a1ee7e9c768fe8f4fb51bd07ea @@ -2570,8 +2701,10 @@ packages: - pypi: ./ name: arbalister version: 0.1.0 - sha256: b3c23e0901a5247469d7f00a61e13669ae82d954e1ae19f5074a2678a84efb23 + sha256: 64957ee8a9f7ed7c972b982dae789b32d263bdafc5aaa9cdce14d6524831ae7b requires_dist: + - adbc-driver-manager + - adbc-driver-sqlite - datafusion>=40.0 - jupyter-server>=2.4.0,<3 - pyarrow>=20.0 @@ -4883,6 +5016,20 @@ packages: - pkg:pypi/importlib-metadata?source=hash-mapping size: 34641 timestamp: 1747934053147 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + sha256: acc1d991837c0afb67c75b77fdc72b4bf022aac71fedd8b9ea45918ac9b08a80 + md5: c85c76dc67d75619a92f51dfbce06992 + depends: + - python >=3.9 + - zipp >=3.1.0 + constrains: + - importlib-resources >=6.5.2,<6.5.3.0a0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-resources?source=hash-mapping + size: 33781 + timestamp: 1736252433366 - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 md5: 9614359868482abba1bd15ce465e3c42 @@ -5504,6 +5651,56 @@ packages: purls: [] size: 1836732 timestamp: 1742370096247 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libadbc-driver-sqlite-1.9.0-hcea63bf_1.conda + sha256: 313d4ab39d50036b0816b01b290174d591e342f83b18eaaa6d21073f408b83e8 + md5: 053a42e10fba063db539a8116d251a44 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libsqlite >=3.51.0,<4.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 194057 + timestamp: 1764032114809 +- conda: https://conda.anaconda.org/conda-forge/osx-64/libadbc-driver-sqlite-1.9.0-h4340512_1.conda + sha256: 098a1ebc11cd605c128cd32434b21d57b0d87777b0a4b7becb120995d1ec7f97 + md5: e243907376ea9c4ad3fb1c1c99baee6f + depends: + - __osx >=11.0 + - libcxx >=19 + - libsqlite >=3.51.0,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 204299 + timestamp: 1764032925091 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libadbc-driver-sqlite-1.9.0-hdd8eb12_1.conda + sha256: 1e40620e0ba150615708143d95c88923614173eb0ebacdf1db7312a9abd8fa74 + md5: db776abb92bddec261c64bf3bc760d26 + depends: + - __osx >=11.0 + - libcxx >=19 + - libsqlite >=3.51.0,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 189010 + timestamp: 1764033020392 +- conda: https://conda.anaconda.org/conda-forge/win-64/libadbc-driver-sqlite-1.9.0-h18ee088_1.conda + sha256: e2f593e5c80c8959a090fd90adca0962927878fe9d392d5db58e30737796be4c + md5: d46c2a845fd78d99872a8bcefa2b217c + depends: + - libsqlite >=3.51.0,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 164032 + timestamp: 1764032117306 - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-20.0.0-h1b9301b_8_cpu.conda build_number: 8 sha256: e218ae6165e6243d8850352640cee57f06a8d05743647918a0370cc5fcc8b602 diff --git a/pyproject.toml b/pyproject.toml index 778a04f..3fe3977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,15 @@ classifiers = [ "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] -dependencies = ["jupyter_server>=2.4.0,<3", "pyarrow>=20.0", "datafusion>=40.0"] dynamic = ["version", "description", "authors", "urls", "keywords"] +dependencies = [ + "jupyter_server>=2.4.0,<3", + "pyarrow>=20.0", + "datafusion>=40.0", + # Optional dependencies + "adbc_driver_manager", + "adbc_driver_sqlite", +] [tool.hatch.version] source = "nodejs" @@ -115,6 +122,8 @@ nodejs = "*" jupyter_server = "*" pyarrow = "*" datafusion = "*" +adbc-driver-manager = "*" +adbc-driver-sqlite = "*" [tool.pixi.pypi-dependencies] arbalister = { path = ".", editable = true } From cf51cbb15ddccd53049867c938be40855cec7d63 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 11:17:49 +0100 Subject: [PATCH 02/10] Add sqlite file type --- arbalister/__init__.py | 1 + arbalister/arrow.py | 61 ++++++++++----------------------- arbalister/file_format.py | 35 +++++++++++++++++++ arbalister/routes.py | 3 +- arbalister/tests/test_routes.py | 11 +++--- data/generate.py | 13 +++---- 6 files changed, 70 insertions(+), 54 deletions(-) create mode 100644 arbalister/file_format.py diff --git a/arbalister/__init__.py b/arbalister/__init__.py index 1c93ea4..51ae515 100644 --- a/arbalister/__init__.py +++ b/arbalister/__init__.py @@ -15,6 +15,7 @@ import jupyterlab.labapp from . import arrow as arrow +from . import file_format as file_format from . import params as params from . import routes as routes diff --git a/arbalister/arrow.py b/arbalister/arrow.py index 3157b36..ec1c42a 100644 --- a/arbalister/arrow.py +++ b/arbalister/arrow.py @@ -1,39 +1,10 @@ -import enum import pathlib -from typing import Any, Callable, Self +from typing import Any, Callable import datafusion as dn import pyarrow as pa - -class FileFormat(enum.StrEnum): - """Known file format that we can read into an Arrow format. - - Todo: - - ADBC (Sqlite/Postgres) - - """ - - Avro = "avro" - Csv = "csv" - Ipc = "ipc" - Orc = "orc" - Parquet = "parquet" - - @classmethod - def from_filename(cls, file: pathlib.Path | str) -> Self: - """Get the file format from a filename extension.""" - file_type = pathlib.Path(file).suffix.removeprefix(".").strip().lower() - - # Match again their default value - if ft := next((ft for ft in FileFormat if str(ft) == file_type), None): - return ft - # Match other known values - match file_type: - case "ipc" | "feather": - return cls.Ipc - raise ValueError(f"Unknown file type {file_type}") - +from . import file_format as ff ReadCallable = Callable[..., dn.DataFrame] @@ -84,7 +55,7 @@ def _write_avro( writer.close() -def get_table_reader(format: FileFormat) -> ReadCallable: +def get_table_reader(format: ff.FileFormat) -> ReadCallable: """Get the datafusion reader factory function for the given format.""" # TODO: datafusion >= 50.0 # def read(ctx: dtfn.SessionContext, path: str | pathlib.Path, *args, **kwargs) -> dtfn.DataFrame: @@ -92,13 +63,13 @@ def get_table_reader(format: FileFormat) -> ReadCallable: # return ctx.read_table(ds, *args, **kwargs) out: ReadCallable match format: - case FileFormat.Avro: + case ff.FileFormat.Avro: out = dn.SessionContext.read_avro - case FileFormat.Csv: + case ff.FileFormat.Csv: out = dn.SessionContext.read_csv - case FileFormat.Parquet: + case ff.FileFormat.Parquet: out = dn.SessionContext.read_parquet - case FileFormat.Ipc: + case ff.FileFormat.Ipc: import pyarrow.feather def read_ipc( @@ -109,7 +80,7 @@ def read_ipc( return ctx.from_arrow(table) out = read_ipc - case FileFormat.Orc: + case ff.FileFormat.Orc: # Watch for https://github.com/datafusion-contrib/datafusion-orc # Evolution for native datafusion reader import pyarrow.orc @@ -121,6 +92,8 @@ def read_orc( return ctx.from_arrow(table) out = read_orc + case ff.FileFormat.Sqlite: + raise NotImplementedError("Sqlite not available in Python Datafusion") return out @@ -128,26 +101,28 @@ def read_orc( WriteCallable = Callable[..., None] -def get_table_writer(format: FileFormat) -> WriteCallable: +def get_table_writer(format: ff.FileFormat) -> WriteCallable: """Get the arrow writer factory function for the given format.""" out: WriteCallable match format: - case FileFormat.Avro: + case ff.FileFormat.Avro: out = _write_avro - case FileFormat.Csv: + case ff.FileFormat.Csv: import pyarrow.csv out = pyarrow.csv.write_csv - case FileFormat.Parquet: + case ff.FileFormat.Parquet: import pyarrow.parquet out = pyarrow.parquet.write_table - case FileFormat.Ipc: + case ff.FileFormat.Ipc: import pyarrow.feather out = pyarrow.feather.write_feather - case FileFormat.Orc: + case ff.FileFormat.Orc: import pyarrow.orc out = pyarrow.orc.write_table + case ff.FileFormat.Sqlite: + raise NotImplementedError("Sqlite not available in Python Datafusion") return out diff --git a/arbalister/file_format.py b/arbalister/file_format.py new file mode 100644 index 0000000..f265b99 --- /dev/null +++ b/arbalister/file_format.py @@ -0,0 +1,35 @@ +import enum +import pathlib +from typing import Self + + +class FileFormat(enum.StrEnum): + """Known file format that we can read into an Arrow format. + + Todo: + - ADBC (Sqlite/Postgres) + + """ + + Avro = "avro" + Csv = "csv" + Ipc = "ipc" + Orc = "orc" + Parquet = "parquet" + Sqlite = "sqlite" + + @classmethod + def from_filename(cls, file: pathlib.Path | str) -> Self: + """Get the file format from a filename extension.""" + file_type = pathlib.Path(file).suffix.removeprefix(".").strip().lower() + + # Match again their default value + if ft := next((ft for ft in FileFormat if str(ft) == file_type), None): + return ft + # Match other known values + match file_type: + case "ipc" | "feather": + return cls.Ipc + case "sqlite3" | "db": + return cls.Sqlite + raise ValueError(f"Unknown file type {file_type}") diff --git a/arbalister/routes.py b/arbalister/routes.py index 4d12ddf..aaf8098 100644 --- a/arbalister/routes.py +++ b/arbalister/routes.py @@ -11,6 +11,7 @@ from jupyter_server.utils import url_path_join from . import arrow as abw +from . import file_format as ff from . import params as params @@ -33,7 +34,7 @@ def dataframe(self, path: str) -> dn.DataFrame: Note: On some file type, the file is read eagerly when calling this method. """ file = self.data_file(path) - read_table = abw.get_table_reader(format=abw.FileFormat.from_filename(file)) + read_table = abw.get_table_reader(format=ff.FileFormat.from_filename(file)) return read_table(self.context, file) def get_query_params_as[T](self, dataclass_type: type[T]) -> T: diff --git a/arbalister/tests/test_routes.py b/arbalister/tests/test_routes.py index 90c1cf0..c851498 100644 --- a/arbalister/tests/test_routes.py +++ b/arbalister/tests/test_routes.py @@ -12,10 +12,13 @@ import arbalister as arb -@pytest.fixture(params=list(arb.arrow.FileFormat), scope="session") -def file_format(request: pytest.FixtureRequest) -> arb.arrow.FileFormat: +@pytest.fixture( + params=[f for f in arb.file_format.FileFormat if f != arb.file_format.FileFormat.Sqlite], + scope="session", +) +def file_format(request: pytest.FixtureRequest) -> arb.file_format.FileFormat: """Parametrize the file format used in the test.""" - out: arb.arrow.FileFormat = request.param + out: arb.file_format.FileFormat = request.param return out @@ -39,7 +42,7 @@ def dummy_table() -> pa.Table: @pytest.fixture def dummy_table_file( - jp_root_dir: pathlib.Path, dummy_table: pa.Table, file_format: arb.arrow.FileFormat + jp_root_dir: pathlib.Path, dummy_table: pa.Table, file_format: arb.file_format.FileFormat ) -> pathlib.Path: """Write the dummy table to file.""" write_table = arb.arrow.get_table_writer(file_format) diff --git a/data/generate.py b/data/generate.py index b14e24e..ec8f2be 100644 --- a/data/generate.py +++ b/data/generate.py @@ -9,6 +9,7 @@ import pyarrow.parquet as paq import arbalister.arrow as aa +from arbalister import file_format as ff MAX_FAKER_ROWS = 100_000 @@ -81,7 +82,7 @@ def sink_coordinate_table( num_rows: int, num_cols: int, path: pathlib.Path, chunk_size: int = 1_000_000 ) -> None: """Write iteratively a table where each cell has its coordinates.""" - assert aa.FileFormat.from_filename(path) == aa.FileFormat.Parquet + assert ff.FileFormat.from_filename(path) == ff.FileFormat.Parquet print("Initializing session context", flush=True) ctx = dn.SessionContext() print("Generating schema", flush=True) @@ -97,7 +98,7 @@ def configure_command_single(cmd: argparse.ArgumentParser) -> argparse.ArgumentP cmd.add_argument( "--output-type", "-t", - choices=[t.name.lower() for t in aa.FileFormat], + choices=[t.name.lower() for t in ff.FileFormat], default=None, help="Output file type", ) @@ -149,7 +150,7 @@ def shuffle_table(table: pa.Table, seed: int | None = None) -> pa.Table: return table.select(col_order).take(row_indices) -def save_table(table: pa.Table, path: pathlib.Path, file_type: aa.FileFormat) -> None: +def save_table(table: pa.Table, path: pathlib.Path, file_type: ff.FileFormat) -> None: """Save a table to file with the given file type.""" path.parent.mkdir(exist_ok=True, parents=True) write_table = aa.get_table_writer(file_type) @@ -165,13 +166,13 @@ def main() -> None: match args.command: case "single": - ft = next((t for t in aa.FileFormat if t.name.lower() == args.output_type), None) + ft = next((t for t in ff.FileFormat if t.name.lower() == args.output_type), None) if ft is None: - ft = aa.FileFormat.from_filename(args.output_file) + ft = ff.FileFormat.from_filename(args.output_file) save_table(shuffle_table(table), args.output_file, ft) case "batch": for p in args.output_file: - ft = aa.FileFormat.from_filename(p) + ft = ff.FileFormat.from_filename(p) save_table(shuffle_table(table), p, ft) case "coordinate": sink_coordinate_table(num_rows=args.num_rows, num_cols=args.num_cols, path=args.output_file) From 894d379bfb5f8d9a83b0a80a6e658fc06f52cdbc Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 16:34:47 +0100 Subject: [PATCH 03/10] Add Sqlite single table support in server --- arbalister/adbc.py | 84 +++++++++++++++++++++++++++++++++ arbalister/arrow.py | 11 ++++- arbalister/tests/test_routes.py | 5 +- 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 arbalister/adbc.py diff --git a/arbalister/adbc.py b/arbalister/adbc.py new file mode 100644 index 0000000..c07b8fe --- /dev/null +++ b/arbalister/adbc.py @@ -0,0 +1,84 @@ +import dataclasses +import pathlib +from typing import Any, Self + +import adbc_driver_sqlite.dbapi as adbc_sqlite +import pyarrow as pa + + +def write_sqlite( + table: pa.Table, + path: str | pathlib.Path, + table_name: str = "Table", + catalog_name: str | None = None, +) -> None: + """Write a table as an Sqlite file.""" + with adbc_sqlite.connect(str(path)) as connection: + with connection.cursor() as cursor: + cursor.adbc_ingest(table_name=table_name, data=table, mode="create", catalog_name=catalog_name) + connection.commit() + + +@dataclasses.dataclass +class SqliteDataFrame: + """A DataFrame plan on a Sqlite file. + + This plan is made to resemble a Apache Datafusion DataFrame despite some performances penalty. + This is because in the Rust Datafusion implementation of as a Sqlite contrib which we would + like to eventually use so this class is only filling in temporarily. + """ + + _path: str + _table_name: str + _schema: pa.Schema + _num_rows: int + _limit: int | None = None + _offset: int | None = None + _select: list[str] | None = None + + @classmethod + def read_sqlite(cls, context: Any, path: pathlib.Path | str, table_name: str | None = None) -> Self: + """Read an Sqlite file metadata and start a new DataFrame plan.""" + with adbc_sqlite.connect(str(path)) as connection: + tables = connection.adbc_get_table_types() + if table_name is None and len(tables) > 0: + table_name = tables[0] + elif table_name not in tables: + raise ValueError(f"Invalid table name {table_name}") + schema = connection.adbc_get_table_schema(table_name) + + with connection.cursor() as cursor: + cursor.execute(f'SELECT COUNT(*) FROM "{table_name}"') + num_rows = cursor.fetchone()[0] # type: ignore[index] + + return cls(_path=str(path), _table_name=table_name, _schema=schema, _num_rows=num_rows) + + def schema(self) -> pa.Schema: + """Return the :py:class:`pyarrow.Schema` of this DataFrame.""" + return self._schema + + def limit(self, count: int, offset: int = 0) -> Self: + """Return a new DataFrame with a limited number of rows.""" + return dataclasses.replace(self, _limit=count, _offset=offset) + + def select(self, *columns: str) -> Self: + """Project arbitrary expressions into a new `DataFrame`.""" + return dataclasses.replace(self, _select=list(columns)) + + def count(self) -> int: + """Return the total number of rows in this DataFrame.""" + return self._num_rows + + def to_arrow_table(self) -> pa.Table: + """Execute the DataFrame and convert it into an Arrow Table.""" + limit = ( + f"LIMIT {self._limit} OFFSET {self._offset}" + if self._limit is not None and self._offset is not None + else "" + ) + columns = self._select if self._select is not None else ["*"] + + with adbc_sqlite.connect(self._path) as connection: + with connection.cursor() as cursor: + cursor.execute(f'SELECT {",".join(columns)} FROM "{self._table_name}" {limit}') + return cursor.fetch_arrow_table() diff --git a/arbalister/arrow.py b/arbalister/arrow.py index ec1c42a..d24882d 100644 --- a/arbalister/arrow.py +++ b/arbalister/arrow.py @@ -93,7 +93,12 @@ def read_orc( out = read_orc case ff.FileFormat.Sqlite: - raise NotImplementedError("Sqlite not available in Python Datafusion") + from . import adbc as adbc + + # FIXME: For now we just pretend SqliteDataFrame is a datafision DataFrame + # Either we integrate it properly into Datafusion, or we create a DataFrame as a + # typing.protocol. + out = adbc.SqliteDataFrame.read_sqlite # type: ignore[assignment] return out @@ -124,5 +129,7 @@ def get_table_writer(format: ff.FileFormat) -> WriteCallable: out = pyarrow.orc.write_table case ff.FileFormat.Sqlite: - raise NotImplementedError("Sqlite not available in Python Datafusion") + from . import adbc as adbc + + out = adbc.write_sqlite return out diff --git a/arbalister/tests/test_routes.py b/arbalister/tests/test_routes.py index c851498..e7922b8 100644 --- a/arbalister/tests/test_routes.py +++ b/arbalister/tests/test_routes.py @@ -12,10 +12,7 @@ import arbalister as arb -@pytest.fixture( - params=[f for f in arb.file_format.FileFormat if f != arb.file_format.FileFormat.Sqlite], - scope="session", -) +@pytest.fixture(params=list(arb.file_format.FileFormat), scope="session") def file_format(request: pytest.FixtureRequest) -> arb.file_format.FileFormat: """Parametrize the file format used in the test.""" out: arb.file_format.FileFormat = request.param From 37d21c5433b49022ae33226b52677e424fd7a99d Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 17:43:30 +0100 Subject: [PATCH 04/10] Test different sqlite table names --- arbalister/adbc.py | 22 ++++++---- arbalister/routes.py | 28 ++++++++++++- arbalister/tests/test_routes.py | 73 +++++++++++++++++++++++---------- 3 files changed, 91 insertions(+), 32 deletions(-) diff --git a/arbalister/adbc.py b/arbalister/adbc.py index c07b8fe..54579d7 100644 --- a/arbalister/adbc.py +++ b/arbalister/adbc.py @@ -1,6 +1,6 @@ import dataclasses import pathlib -from typing import Any, Self +from typing import Any, Literal, Self import adbc_driver_sqlite.dbapi as adbc_sqlite import pyarrow as pa @@ -11,11 +11,12 @@ def write_sqlite( path: str | pathlib.Path, table_name: str = "Table", catalog_name: str | None = None, + mode: Literal["append", "create", "replace", "create_append"] = "create", ) -> None: """Write a table as an Sqlite file.""" with adbc_sqlite.connect(str(path)) as connection: with connection.cursor() as cursor: - cursor.adbc_ingest(table_name=table_name, data=table, mode="create", catalog_name=catalog_name) + cursor.adbc_ingest(table_name=table_name, data=table, mode=mode, catalog_name=catalog_name) connection.commit() @@ -40,17 +41,20 @@ class SqliteDataFrame: def read_sqlite(cls, context: Any, path: pathlib.Path | str, table_name: str | None = None) -> Self: """Read an Sqlite file metadata and start a new DataFrame plan.""" with adbc_sqlite.connect(str(path)) as connection: - tables = connection.adbc_get_table_types() - if table_name is None and len(tables) > 0: - table_name = tables[0] - elif table_name not in tables: - raise ValueError(f"Invalid table name {table_name}") - schema = connection.adbc_get_table_schema(table_name) - with connection.cursor() as cursor: + cursor.execute("SELECT name FROM sqlite_master WHERE type='table'") + tables = [row for (row,) in cursor.fetchall()] + + if table_name is None and len(tables) > 0: + table_name = tables[0] + if table_name not in tables or table_name is None: + raise ValueError(f"Invalid table name {table_name}") + cursor.execute(f'SELECT COUNT(*) FROM "{table_name}"') num_rows = cursor.fetchone()[0] # type: ignore[index] + schema = connection.adbc_get_table_schema(table_name) + return cls(_path=str(path), _table_name=table_name, _schema=schema, _num_rows=num_rows) def schema(self) -> pa.Schema: diff --git a/arbalister/routes.py b/arbalister/routes.py index aaf8098..0cb2ce1 100644 --- a/arbalister/routes.py +++ b/arbalister/routes.py @@ -15,6 +15,21 @@ from . import params as params +@dataclasses.dataclass(frozen=True, slots=True) +class SqliteReadParams: + """Query parameter for the Sqlite reader.""" + + table_name: str | None = None + + +@dataclasses.dataclass(frozen=True, slots=True) +class NoReadParams: + """Query parameter for readers with no parameters.""" + + +FileReadParams = SqliteReadParams | NoReadParams + + class BaseRouteHandler(jupyter_server.base.handlers.APIHandler): """A base handler to share common methods.""" @@ -34,13 +49,22 @@ def dataframe(self, path: str) -> dn.DataFrame: Note: On some file type, the file is read eagerly when calling this method. """ file = self.data_file(path) - read_table = abw.get_table_reader(format=ff.FileFormat.from_filename(file)) - return read_table(self.context, file) + file_format = ff.FileFormat.from_filename(file) + file_params = self.get_file_read_params(file_format) + read_table = abw.get_table_reader(format=file_format) + return read_table(self.context, file, **dataclasses.asdict(file_params)) def get_query_params_as[T](self, dataclass_type: type[T]) -> T: """Extract query parameters into a dataclass type.""" return params.build_dataclass(dataclass_type, self.get_query_argument) + def get_file_read_params(self, file_format: ff.FileFormat) -> FileReadParams: + """Read the parameters associated with the relevant file format.""" + match file_format: + case ff.FileFormat.Sqlite: + return self.get_query_params_as(SqliteReadParams) + return NoReadParams() + @dataclasses.dataclass(frozen=True, slots=True) class IpcParams: diff --git a/arbalister/tests/test_routes.py b/arbalister/tests/test_routes.py index e7922b8..dd42aa8 100644 --- a/arbalister/tests/test_routes.py +++ b/arbalister/tests/test_routes.py @@ -19,32 +19,61 @@ def file_format(request: pytest.FixtureRequest) -> arb.file_format.FileFormat: return out -DUMMY_TABLE_ROW_COUNT: Final = 10 -DUMMY_TABLE_COL_COUNT: Final = 4 +DUMMY_TABLE_1_ROW_COUNT: Final = 10 +DUMMY_TABLE_1_COL_COUNT: Final = 4 @pytest.fixture(scope="module") -def dummy_table() -> pa.Table: +def dummy_table_1() -> pa.Table: """Generate a table with fake data.""" data = { - "lower": random.choices(string.ascii_lowercase, k=DUMMY_TABLE_ROW_COUNT), - "sequence": list(range(DUMMY_TABLE_ROW_COUNT)), - "upper": random.choices(string.ascii_uppercase, k=DUMMY_TABLE_ROW_COUNT), - "number": [random.random() for _ in range(DUMMY_TABLE_ROW_COUNT)], + "lower": random.choices(string.ascii_lowercase, k=DUMMY_TABLE_1_ROW_COUNT), + "sequence": list(range(DUMMY_TABLE_1_ROW_COUNT)), + "upper": random.choices(string.ascii_uppercase, k=DUMMY_TABLE_1_ROW_COUNT), + "number": [random.random() for _ in range(DUMMY_TABLE_1_ROW_COUNT)], } table = pa.table(data) - assert len(table.schema) == DUMMY_TABLE_COL_COUNT + assert len(table.schema) == DUMMY_TABLE_1_COL_COUNT + return table + + +DUMMY_TABLE_2_ROW_COUNT = 13 +DUMMY_TABLE_2_COL_COUNT = 5 + + +@pytest.fixture(scope="module") +def dummy_table_2() -> pa.Table: + """Generate a table with different fake data.""" + data = { + "id": list(range(DUMMY_TABLE_2_ROW_COUNT)), + "flag": [random.choice([True, False]) for _ in range(DUMMY_TABLE_2_ROW_COUNT)], + "letter": random.choices(string.ascii_letters, k=DUMMY_TABLE_2_ROW_COUNT), + "score": [random.randint(0, 100) for _ in range(DUMMY_TABLE_2_ROW_COUNT)], + "timestamp": [random.randint(1_600_000_000, 1_700_000_000) for _ in range(DUMMY_TABLE_2_ROW_COUNT)], + } + table = pa.table(data) + assert len(table.schema) == DUMMY_TABLE_2_COL_COUNT return table @pytest.fixture def dummy_table_file( - jp_root_dir: pathlib.Path, dummy_table: pa.Table, file_format: arb.file_format.FileFormat + jp_root_dir: pathlib.Path, + dummy_table_1: pa.Table, + dummy_table_2: pa.Table, + file_format: arb.file_format.FileFormat, ) -> pathlib.Path: """Write the dummy table to file.""" write_table = arb.arrow.get_table_writer(file_format) table_path = jp_root_dir / f"test.{str(file_format).lower()}" - write_table(dummy_table, table_path) + + match file_format: + case arb.file_format.FileFormat.Sqlite: + write_table(dummy_table_1, table_path, table_name="dummy_table_1", mode="create_append") + write_table(dummy_table_2, table_path, table_name="dummy_table_2", mode="create_append") + case _: + write_table(dummy_table_1, table_path) + return table_path.relative_to(jp_root_dir) @@ -58,23 +87,23 @@ def dummy_table_file( # Limit only number of rows arb.routes.IpcParams(row_chunk=0, row_chunk_size=3), arb.routes.IpcParams(row_chunk=1, row_chunk_size=2), - arb.routes.IpcParams(row_chunk=0, row_chunk_size=DUMMY_TABLE_ROW_COUNT), - arb.routes.IpcParams(row_chunk=1, row_chunk_size=DUMMY_TABLE_ROW_COUNT // 2 + 1), + arb.routes.IpcParams(row_chunk=0, row_chunk_size=DUMMY_TABLE_1_ROW_COUNT), + arb.routes.IpcParams(row_chunk=1, row_chunk_size=DUMMY_TABLE_1_ROW_COUNT // 2 + 1), # Limit only number of cols arb.routes.IpcParams(col_chunk=0, col_chunk_size=3), arb.routes.IpcParams(col_chunk=1, col_chunk_size=2), - arb.routes.IpcParams(col_chunk=0, col_chunk_size=DUMMY_TABLE_COL_COUNT), - arb.routes.IpcParams(col_chunk=1, col_chunk_size=DUMMY_TABLE_COL_COUNT // 2 + 1), + arb.routes.IpcParams(col_chunk=0, col_chunk_size=DUMMY_TABLE_1_COL_COUNT), + arb.routes.IpcParams(col_chunk=1, col_chunk_size=DUMMY_TABLE_1_COL_COUNT // 2 + 1), # Limit both arb.routes.IpcParams( row_chunk=0, row_chunk_size=3, col_chunk=1, - col_chunk_size=DUMMY_TABLE_COL_COUNT // 2 + 1, + col_chunk_size=DUMMY_TABLE_1_COL_COUNT // 2 + 1, ), arb.routes.IpcParams( row_chunk=0, - row_chunk_size=DUMMY_TABLE_ROW_COUNT, + row_chunk_size=DUMMY_TABLE_1_ROW_COUNT, col_chunk=1, col_chunk_size=2, ), @@ -87,7 +116,7 @@ def dummy_table_file( ) async def test_ipc_route_limit_row( jp_fetch: JpFetch, - dummy_table: pa.Table, + dummy_table_1: pa.Table, dummy_table_file: pathlib.Path, params: arb.routes.IpcParams, ) -> None: @@ -102,7 +131,7 @@ async def test_ipc_route_limit_row( assert response.headers["Content-Type"] == "application/vnd.apache.arrow.stream" payload = pa.ipc.open_stream(response.body).read_all() - expected = dummy_table + expected = dummy_table_1 # Row slicing if (size := params.row_chunk_size) is not None and (cidx := params.row_chunk) is not None: @@ -122,7 +151,9 @@ async def test_ipc_route_limit_row( assert expected.cast(payload.schema) == payload -async def test_stats_route(jp_fetch: JpFetch, dummy_table: pa.Table, dummy_table_file: pathlib.Path) -> None: +async def test_stats_route( + jp_fetch: JpFetch, dummy_table_1: pa.Table, dummy_table_file: pathlib.Path +) -> None: """Test fetching a file returns the correct metadata in Json.""" response = await jp_fetch("arrow/stats/", str(dummy_table_file)) @@ -130,5 +161,5 @@ async def test_stats_route(jp_fetch: JpFetch, dummy_table: pa.Table, dummy_table assert response.headers["Content-Type"] == "application/json; charset=UTF-8" payload = json.loads(response.body) - assert payload["num_cols"] == len(dummy_table.schema) - assert payload["num_rows"] == dummy_table.num_rows + assert payload["num_cols"] == len(dummy_table_1.schema) + assert payload["num_rows"] == dummy_table_1.num_rows From bfed3505359514e0d914e822d71add44518f378a Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 18:13:39 +0100 Subject: [PATCH 05/10] Refactor ipc_param fixture --- arbalister/tests/test_routes.py | 52 ++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/arbalister/tests/test_routes.py b/arbalister/tests/test_routes.py index dd42aa8..83a182b 100644 --- a/arbalister/tests/test_routes.py +++ b/arbalister/tests/test_routes.py @@ -80,51 +80,57 @@ def dummy_table_file( JpFetch = Callable[..., Awaitable[tornado.httpclient.HTTPResponse]] -@pytest.mark.parametrize( - "params", - [ - arb.routes.IpcParams(), +@pytest.fixture( + params=[ + # No limits + lambda table: arb.routes.IpcParams(), # Limit only number of rows - arb.routes.IpcParams(row_chunk=0, row_chunk_size=3), - arb.routes.IpcParams(row_chunk=1, row_chunk_size=2), - arb.routes.IpcParams(row_chunk=0, row_chunk_size=DUMMY_TABLE_1_ROW_COUNT), - arb.routes.IpcParams(row_chunk=1, row_chunk_size=DUMMY_TABLE_1_ROW_COUNT // 2 + 1), + lambda table: arb.routes.IpcParams(row_chunk=0, row_chunk_size=3), + lambda table: arb.routes.IpcParams(row_chunk=1, row_chunk_size=2), + lambda table: arb.routes.IpcParams(row_chunk=0, row_chunk_size=table.num_rows), + lambda table: arb.routes.IpcParams(row_chunk=1, row_chunk_size=table.num_rows // 2 + 1), # Limit only number of cols - arb.routes.IpcParams(col_chunk=0, col_chunk_size=3), - arb.routes.IpcParams(col_chunk=1, col_chunk_size=2), - arb.routes.IpcParams(col_chunk=0, col_chunk_size=DUMMY_TABLE_1_COL_COUNT), - arb.routes.IpcParams(col_chunk=1, col_chunk_size=DUMMY_TABLE_1_COL_COUNT // 2 + 1), + lambda table: arb.routes.IpcParams(col_chunk=0, col_chunk_size=3), + lambda table: arb.routes.IpcParams(col_chunk=1, col_chunk_size=2), + lambda table: arb.routes.IpcParams(col_chunk=0, col_chunk_size=table.num_columns), + lambda table: arb.routes.IpcParams(col_chunk=1, col_chunk_size=table.num_columns // 2 + 1), # Limit both - arb.routes.IpcParams( + lambda table: arb.routes.IpcParams( row_chunk=0, row_chunk_size=3, col_chunk=1, - col_chunk_size=DUMMY_TABLE_1_COL_COUNT // 2 + 1, + col_chunk_size=table.num_columns // 2 + 1, ), - arb.routes.IpcParams( + lambda table: arb.routes.IpcParams( row_chunk=0, - row_chunk_size=DUMMY_TABLE_1_ROW_COUNT, + row_chunk_size=table.num_rows, col_chunk=1, col_chunk_size=2, ), # Schema only - arb.routes.IpcParams( + lambda table: arb.routes.IpcParams( row_chunk=0, row_chunk_size=0, ), - ], + ] ) -async def test_ipc_route_limit_row( +def ipc_params(request: pytest.FixtureRequest, dummy_table_1: pa.Table) -> arb.routes.IpcParams: + """Parameters used to select the IPC data in the response.""" + make_table: Callable[[pa.Table], arb.routes.IpcParams] = request.param + return make_table(dummy_table_1) + + +async def test_ipc_route_limit( jp_fetch: JpFetch, dummy_table_1: pa.Table, dummy_table_file: pathlib.Path, - params: arb.routes.IpcParams, + ipc_params: arb.routes.IpcParams, ) -> None: """Test fetching a file returns the limited rows and columns in IPC.""" response = await jp_fetch( "arrow/stream", str(dummy_table_file), - params={k: v for k, v in dataclasses.asdict(params).items() if v is not None}, + params={k: v for k, v in dataclasses.asdict(ipc_params).items() if v is not None}, ) assert response.code == 200 @@ -134,13 +140,13 @@ async def test_ipc_route_limit_row( expected = dummy_table_1 # Row slicing - if (size := params.row_chunk_size) is not None and (cidx := params.row_chunk) is not None: + if (size := ipc_params.row_chunk_size) is not None and (cidx := ipc_params.row_chunk) is not None: expected_num_rows = min((size * (cidx + 1)), expected.num_rows) - (size * cidx) assert payload.num_rows == expected_num_rows expected = expected.slice(cidx * size, size) # Col slicing - if (size := params.col_chunk_size) is not None and (cidx := params.col_chunk) is not None: + if (size := ipc_params.col_chunk_size) is not None and (cidx := ipc_params.col_chunk) is not None: expected_num_cols = min((size * (cidx + 1)), len(expected.schema)) - (size * cidx) assert len(payload.schema) == expected_num_cols col_names = expected.schema.names From 251a12631b8ab23c41d5adb7c3fd81704c43c4c9 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 18:19:54 +0100 Subject: [PATCH 06/10] Refactor constants --- arbalister/tests/test_routes.py | 34 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/arbalister/tests/test_routes.py b/arbalister/tests/test_routes.py index 83a182b..b711653 100644 --- a/arbalister/tests/test_routes.py +++ b/arbalister/tests/test_routes.py @@ -3,7 +3,7 @@ import pathlib import random import string -from typing import Awaitable, Callable, Final +from typing import Awaitable, Callable import pyarrow as pa import pytest @@ -19,40 +19,30 @@ def file_format(request: pytest.FixtureRequest) -> arb.file_format.FileFormat: return out -DUMMY_TABLE_1_ROW_COUNT: Final = 10 -DUMMY_TABLE_1_COL_COUNT: Final = 4 - - @pytest.fixture(scope="module") -def dummy_table_1() -> pa.Table: +def dummy_table_1(num_rows: int = 10) -> pa.Table: """Generate a table with fake data.""" data = { - "lower": random.choices(string.ascii_lowercase, k=DUMMY_TABLE_1_ROW_COUNT), - "sequence": list(range(DUMMY_TABLE_1_ROW_COUNT)), - "upper": random.choices(string.ascii_uppercase, k=DUMMY_TABLE_1_ROW_COUNT), - "number": [random.random() for _ in range(DUMMY_TABLE_1_ROW_COUNT)], + "lower": random.choices(string.ascii_lowercase, k=num_rows), + "sequence": list(range(num_rows)), + "upper": random.choices(string.ascii_uppercase, k=num_rows), + "number": [random.random() for _ in range(num_rows)], } table = pa.table(data) - assert len(table.schema) == DUMMY_TABLE_1_COL_COUNT return table -DUMMY_TABLE_2_ROW_COUNT = 13 -DUMMY_TABLE_2_COL_COUNT = 5 - - @pytest.fixture(scope="module") -def dummy_table_2() -> pa.Table: +def dummy_table_2(num_rows: int = 13) -> pa.Table: """Generate a table with different fake data.""" data = { - "id": list(range(DUMMY_TABLE_2_ROW_COUNT)), - "flag": [random.choice([True, False]) for _ in range(DUMMY_TABLE_2_ROW_COUNT)], - "letter": random.choices(string.ascii_letters, k=DUMMY_TABLE_2_ROW_COUNT), - "score": [random.randint(0, 100) for _ in range(DUMMY_TABLE_2_ROW_COUNT)], - "timestamp": [random.randint(1_600_000_000, 1_700_000_000) for _ in range(DUMMY_TABLE_2_ROW_COUNT)], + "id": list(range(num_rows)), + "flag": [random.choice([True, False]) for _ in range(num_rows)], + "letter": random.choices(string.ascii_letters, k=num_rows), + "score": [random.randint(0, 100) for _ in range(num_rows)], + "timestamp": [random.randint(1_600_000_000, 1_700_000_000) for _ in range(num_rows)], } table = pa.table(data) - assert len(table.schema) == DUMMY_TABLE_2_COL_COUNT return table From 91b2b4003d4fe77e61216b72c81eeee43bb2976d Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 18:52:05 +0100 Subject: [PATCH 07/10] Test sqlite table_name in request --- arbalister/tests/test_routes.py | 79 +++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 13 deletions(-) diff --git a/arbalister/tests/test_routes.py b/arbalister/tests/test_routes.py index b711653..182c108 100644 --- a/arbalister/tests/test_routes.py +++ b/arbalister/tests/test_routes.py @@ -10,15 +10,46 @@ import tornado import arbalister as arb +import arbalister.file_format as ff -@pytest.fixture(params=list(arb.file_format.FileFormat), scope="session") -def file_format(request: pytest.FixtureRequest) -> arb.file_format.FileFormat: - """Parametrize the file format used in the test.""" - out: arb.file_format.FileFormat = request.param +@pytest.fixture( + params=[ + (ff.FileFormat.Avro, arb.routes.NoReadParams()), + (ff.FileFormat.Csv, arb.routes.NoReadParams()), + (ff.FileFormat.Ipc, arb.routes.NoReadParams()), + (ff.FileFormat.Orc, arb.routes.NoReadParams()), + (ff.FileFormat.Parquet, arb.routes.NoReadParams()), + (ff.FileFormat.Sqlite, arb.routes.NoReadParams()), + (ff.FileFormat.Sqlite, arb.routes.SqliteReadParams(table_name="SuperData")), + ], + ids=lambda f_p: f"{f_p[0].value}-{dataclasses.asdict(f_p[1])}", + scope="module", +) +def file_format_and_params(request: pytest.FixtureRequest) -> tuple[ff.FileFormat, arb.routes.FileReadParams]: + """Parametrize the file format and file parameters used in the tests. + + This is used to to build test cases with a give set of parameters since each file format may be tested + with a different number of parameters. + """ + out: tuple[ff.FileFormat, arb.routes.FileReadParams] = request.param return out +@pytest.fixture(scope="module") +def file_format(file_format_and_params: tuple[ff.FileFormat, arb.routes.FileReadParams]) -> ff.FileFormat: + """Extract the the file format fixture value used in the tests.""" + return file_format_and_params[0] + + +@pytest.fixture(scope="module") +def file_params( + file_format_and_params: tuple[ff.FileFormat, arb.routes.FileReadParams], +) -> arb.routes.FileReadParams: + """Extract the the file parameters fixture value used in the tests.""" + return file_format_and_params[1] + + @pytest.fixture(scope="module") def dummy_table_1(num_rows: int = 10) -> pa.Table: """Generate a table with fake data.""" @@ -51,16 +82,22 @@ def dummy_table_file( jp_root_dir: pathlib.Path, dummy_table_1: pa.Table, dummy_table_2: pa.Table, - file_format: arb.file_format.FileFormat, + file_format: ff.FileFormat, + file_params: arb.routes.FileReadParams, ) -> pathlib.Path: """Write the dummy table to file.""" write_table = arb.arrow.get_table_writer(file_format) table_path = jp_root_dir / f"test.{str(file_format).lower()}" match file_format: - case arb.file_format.FileFormat.Sqlite: + case ff.FileFormat.Sqlite: write_table(dummy_table_1, table_path, table_name="dummy_table_1", mode="create_append") - write_table(dummy_table_2, table_path, table_name="dummy_table_2", mode="create_append") + table_2_name = ( + file_params.table_name + if isinstance(file_params, arb.routes.SqliteReadParams) + else "dummy_table_2" + ) + write_table(dummy_table_2, table_path, table_name=table_2_name, mode="create_append") case _: write_table(dummy_table_1, table_path) @@ -113,21 +150,27 @@ def ipc_params(request: pytest.FixtureRequest, dummy_table_1: pa.Table) -> arb.r async def test_ipc_route_limit( jp_fetch: JpFetch, dummy_table_1: pa.Table, + dummy_table_2: pa.Table, dummy_table_file: pathlib.Path, ipc_params: arb.routes.IpcParams, + file_params: arb.routes.SqliteReadParams, ) -> None: """Test fetching a file returns the limited rows and columns in IPC.""" response = await jp_fetch( "arrow/stream", str(dummy_table_file), - params={k: v for k, v in dataclasses.asdict(ipc_params).items() if v is not None}, + params={ + k: v + for k, v in {**dataclasses.asdict(ipc_params), **dataclasses.asdict(file_params)}.items() + if v is not None + }, ) assert response.code == 200 assert response.headers["Content-Type"] == "application/vnd.apache.arrow.stream" payload = pa.ipc.open_stream(response.body).read_all() - expected = dummy_table_1 + expected = dummy_table_2 if isinstance(file_params, arb.routes.SqliteReadParams) else dummy_table_1 # Row slicing if (size := ipc_params.row_chunk_size) is not None and (cidx := ipc_params.row_chunk) is not None: @@ -148,14 +191,24 @@ async def test_ipc_route_limit( async def test_stats_route( - jp_fetch: JpFetch, dummy_table_1: pa.Table, dummy_table_file: pathlib.Path + jp_fetch: JpFetch, + dummy_table_1: pa.Table, + dummy_table_2: pa.Table, + dummy_table_file: pathlib.Path, + file_params: arb.routes.SqliteReadParams, ) -> None: """Test fetching a file returns the correct metadata in Json.""" - response = await jp_fetch("arrow/stats/", str(dummy_table_file)) + response = await jp_fetch( + "arrow/stats/", + str(dummy_table_file), + params={k: v for k, v in dataclasses.asdict(file_params).items() if v is not None}, + ) assert response.code == 200 assert response.headers["Content-Type"] == "application/json; charset=UTF-8" + expected = dummy_table_2 if isinstance(file_params, arb.routes.SqliteReadParams) else dummy_table_1 + payload = json.loads(response.body) - assert payload["num_cols"] == len(dummy_table_1.schema) - assert payload["num_rows"] == dummy_table_1.num_rows + assert payload["num_cols"] == len(expected.schema) + assert payload["num_rows"] == expected.num_rows From c77999fbc024169b6f65ee8024eb564848858ec8 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 18:59:35 +0100 Subject: [PATCH 08/10] Refactor target table fixture in test --- arbalister/tests/test_routes.py | 44 +++++++++++++++++---------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/arbalister/tests/test_routes.py b/arbalister/tests/test_routes.py index 182c108..9b571b0 100644 --- a/arbalister/tests/test_routes.py +++ b/arbalister/tests/test_routes.py @@ -21,7 +21,7 @@ (ff.FileFormat.Orc, arb.routes.NoReadParams()), (ff.FileFormat.Parquet, arb.routes.NoReadParams()), (ff.FileFormat.Sqlite, arb.routes.NoReadParams()), - (ff.FileFormat.Sqlite, arb.routes.SqliteReadParams(table_name="SuperData")), + (ff.FileFormat.Sqlite, arb.routes.SqliteReadParams(table_name="dummy_table_2")), ], ids=lambda f_p: f"{f_p[0].value}-{dataclasses.asdict(f_p[1])}", scope="module", @@ -77,8 +77,19 @@ def dummy_table_2(num_rows: int = 13) -> pa.Table: return table +@pytest.fixture(scope="module") +def full_table(file_params: ff.FileFormat, dummy_table_1: pa.Table, dummy_table_2: pa.Table) -> pa.Table: + """Return the full table on which we are executed queries.""" + if isinstance(file_params, arb.routes.SqliteReadParams): + return { + "dummy_table_1": dummy_table_1, + "dummy_table_2": dummy_table_2, + }[file_params.table_name] + return dummy_table_1 + + @pytest.fixture -def dummy_table_file( +def table_file( jp_root_dir: pathlib.Path, dummy_table_1: pa.Table, dummy_table_2: pa.Table, @@ -92,12 +103,7 @@ def dummy_table_file( match file_format: case ff.FileFormat.Sqlite: write_table(dummy_table_1, table_path, table_name="dummy_table_1", mode="create_append") - table_2_name = ( - file_params.table_name - if isinstance(file_params, arb.routes.SqliteReadParams) - else "dummy_table_2" - ) - write_table(dummy_table_2, table_path, table_name=table_2_name, mode="create_append") + write_table(dummy_table_2, table_path, table_name="dummy_table_2", mode="create_append") case _: write_table(dummy_table_1, table_path) @@ -149,16 +155,15 @@ def ipc_params(request: pytest.FixtureRequest, dummy_table_1: pa.Table) -> arb.r async def test_ipc_route_limit( jp_fetch: JpFetch, - dummy_table_1: pa.Table, - dummy_table_2: pa.Table, - dummy_table_file: pathlib.Path, + full_table: pa.Table, + table_file: pathlib.Path, ipc_params: arb.routes.IpcParams, file_params: arb.routes.SqliteReadParams, ) -> None: """Test fetching a file returns the limited rows and columns in IPC.""" response = await jp_fetch( "arrow/stream", - str(dummy_table_file), + str(table_file), params={ k: v for k, v in {**dataclasses.asdict(ipc_params), **dataclasses.asdict(file_params)}.items() @@ -170,7 +175,7 @@ async def test_ipc_route_limit( assert response.headers["Content-Type"] == "application/vnd.apache.arrow.stream" payload = pa.ipc.open_stream(response.body).read_all() - expected = dummy_table_2 if isinstance(file_params, arb.routes.SqliteReadParams) else dummy_table_1 + expected = full_table # Row slicing if (size := ipc_params.row_chunk_size) is not None and (cidx := ipc_params.row_chunk) is not None: @@ -192,23 +197,20 @@ async def test_ipc_route_limit( async def test_stats_route( jp_fetch: JpFetch, - dummy_table_1: pa.Table, - dummy_table_2: pa.Table, - dummy_table_file: pathlib.Path, + full_table: pa.Table, + table_file: pathlib.Path, file_params: arb.routes.SqliteReadParams, ) -> None: """Test fetching a file returns the correct metadata in Json.""" response = await jp_fetch( "arrow/stats/", - str(dummy_table_file), + str(table_file), params={k: v for k, v in dataclasses.asdict(file_params).items() if v is not None}, ) assert response.code == 200 assert response.headers["Content-Type"] == "application/json; charset=UTF-8" - expected = dummy_table_2 if isinstance(file_params, arb.routes.SqliteReadParams) else dummy_table_1 - payload = json.loads(response.body) - assert payload["num_cols"] == len(expected.schema) - assert payload["num_rows"] == expected.num_rows + assert payload["num_cols"] == len(full_table.schema) + assert payload["num_rows"] == full_table.num_rows From 5e1d5d8530cde953e4654418df422276c5c468cf Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 19:13:23 +0100 Subject: [PATCH 09/10] Add more file extensions --- arbalister/file_format.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arbalister/file_format.py b/arbalister/file_format.py index f265b99..d2d687a 100644 --- a/arbalister/file_format.py +++ b/arbalister/file_format.py @@ -30,6 +30,6 @@ def from_filename(cls, file: pathlib.Path | str) -> Self: match file_type: case "ipc" | "feather": return cls.Ipc - case "sqlite3" | "db": + case "sqlite3" | "db" | ".db3", ".s3db", ".sl3": return cls.Sqlite raise ValueError(f"Unknown file type {file_type}") From 36242b11f8b99ac663f255fb56fddf32475faf72 Mon Sep 17 00:00:00 2001 From: AntoinePrv Date: Mon, 15 Dec 2025 19:19:12 +0100 Subject: [PATCH 10/10] Add generation of sqlite file --- pixi.lock | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pixi.lock b/pixi.lock index bea4c2a..8d20e59 100644 --- a/pixi.lock +++ b/pixi.lock @@ -2701,7 +2701,7 @@ packages: - pypi: ./ name: arbalister version: 0.1.0 - sha256: 64957ee8a9f7ed7c972b982dae789b32d263bdafc5aaa9cdce14d6524831ae7b + sha256: 3173db325467de56d1c311749ceec1fa1e50196d0cdd0a6bb5d201ee2ed9c8c7 requires_dist: - adbc-driver-manager - adbc-driver-sqlite diff --git a/pyproject.toml b/pyproject.toml index 3fe3977..8d9dafc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -229,6 +229,7 @@ python data/generate.py batch \ -o data/gen/test.ipc \ -o data/gen/test.orc \ -o data/gen/test.parquet \ + -o data/gen/test.sqlite \ """ outputs = ["data/gen/**/test.*"] description = """Generate different data files to use in development in data/gen."""