This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 3559a7ce56 [python] Centralize PyPaimon packaging configuration 
(#10241)
3559a7ce56 is described below

commit 3559a7ce56b9d42305d513b381f8525b5ed08c88
Author: Zouxxyy <[email protected]>
AuthorDate: Sat Oct 3 17:04:38 2026 +0800

    [python] Centralize PyPaimon packaging configuration (#10241)
---
 .github/workflows/ci-python.yml                    | 153 ++++-----
 .github/workflows/release-python-publish.yml       |   2 +-
 .github/workflows/release-python.yml               |  16 +-
 .github/workflows/release.yml                      |   2 +-
 .gitignore                                         |   1 +
 docs/docs/project/creating-a-release.md            |   9 +-
 docs/docs/project/releases.md                      |   2 +-
 docs/docs/project/verifying-a-release-candidate.md |  14 +-
 docs/docs/pypaimon/installation.md                 |  18 +-
 paimon-python/MANIFEST.in                          |   2 +-
 paimon-python/README.md                            |  37 ++-
 paimon-python/conftest.py                          |   3 +-
 paimon-python/dev/check-licensing.sh               |   2 +-
 paimon-python/dev/install.py                       |  71 +++++
 paimon-python/dev/lint-python.sh                   |   6 +-
 paimon-python/dev/requirements-dev.txt             |  41 ---
 paimon-python/dev/requirements.txt                 |  44 ---
 paimon-python/dev/run_mixed_tests.sh               |  25 +-
 paimon-python/dev/test_packaging.py                | 212 +++++++++++++
 .../{MANIFEST.in => pypaimon/_version.py}          |  14 +-
 paimon-python/pypaimon/build_info.py               |  48 ++-
 paimon-python/pypaimon/tests/build_info_test.py    |   7 +-
 .../tests/ray_data_evolution_merge_into_test.py    |   3 +-
 .../pypaimon/tests/rest/rest_read_write_test.py    |  17 +-
 paimon-python/pyproject.toml                       | 211 +++++++++++++
 paimon-python/setup.py                             | 341 +++------------------
 26 files changed, 704 insertions(+), 597 deletions(-)

diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml
index fddb1a4a6d..952a1173a5 100644
--- a/.github/workflows/ci-python.yml
+++ b/.github/workflows/ci-python.yml
@@ -27,7 +27,6 @@ permissions:
 env:
   JDK_VERSION: 8
   MAVEN_OPTS: -Dmaven.wagon.httpconnectionManager.ttlSeconds=30 
-Dmaven.wagon.http.retryHandler.requestSentEnabled=true
-  LUMINA_DATA_VERSION: 0.1.0
 
 
 jobs:
@@ -95,19 +94,26 @@ jobs:
       - name: Verify Python version
         run: python --version
 
-      - name: Verify requirements.txt dependencies can be installed
+      - name: Verify Python distributions
+        run: |
+          # Build tools and legacy Flake8 require incompatible 
importlib-metadata versions.
+          python -m venv "${RUNNER_TEMP}/paimon-packaging"
+          "${RUNNER_TEMP}/paimon-packaging/bin/python" -m pip install 
--upgrade pip setuptools wheel packaging tomli build
+          "${RUNNER_TEMP}/paimon-packaging/bin/python" 
paimon-python/dev/test_packaging.py
+
+      - name: Verify core package dependencies can be installed
         shell: bash
         run: |
           cd paimon-python
           python -m pip install --upgrade pip
           TEMP_DIR=$(mktemp -d)
-          python -m pip install -r dev/requirements.txt --target "$TEMP_DIR" 
|| {
-            echo "ERROR: Failed to resolve dependencies from 
dev/requirements.txt"
+          python -m pip install . --target "$TEMP_DIR" || {
+            echo "ERROR: Failed to install core PyPaimon dependencies"
             rm -rf "$TEMP_DIR"
             exit 1
           }
           rm -rf "$TEMP_DIR"
-          echo "✓ dev/requirements.txt can be resolved on this Python"
+          echo "✓ Core PyPaimon dependencies resolve on this Python"
 
       - name: Build Java
         run: |
@@ -118,57 +124,20 @@ jobs:
       - name: Install Python dependencies
         shell: bash
         run: |
-          df -h
-          if [[ "${{ matrix.python-version }}" == "3.6.15" ]]; then
-            python -m pip install --upgrade pip==21.3.1
-            python --version
-            python -m pip install --no-cache-dir pyroaring 
readerwriterlock==1.0.9 'fsspec==2021.10.1' 'cachetools==4.2.4' 
'ossfs==2021.8.0' pyarrow==6.0.1 pandas==1.1.5 'polars==0.9.12' 
'fastavro==1.4.7' zstandard==0.19.0 dataclasses==0.8.0 flake8 pytest 
py4j==0.10.9.9 requests parameterized==0.8.1 datasketches==4.1.0 2>&1 >/dev/null
-            python -m pip install 'lumina-data>=${{ env.LUMINA_DATA_VERSION 
}}' -i https://pypi.org/simple/
-          elif [[ "${{ matrix.python-version }}" == "3.7" ]]; then
-            # 3.7 installs the version-pinned set declared for 3.7 in 
dev/requirements.txt.
-            # ray/lance/daft/torch have no 3.7 wheels; those tests are 
importorskip-ed (as on 3.6.15).
-            python -m pip install --upgrade pip
-            python -m pip install -r paimon-python/dev/requirements.txt
-            python -m pip install flake8==4.0.1 'pytest~=7.0' py4j==0.10.9.9 
parameterized==0.9.0 datasketches==4.1.0
-            python -m pip install 'lumina-data>=${{ env.LUMINA_DATA_VERSION 
}}' -i https://pypi.org/simple/
+          python -m pip install --upgrade pip
+          if python -c "import sys; sys.exit(0 if sys.version_info < (3, 8) 
else 1)"; then
+            # Old pip has no --group; read the same TOML with the 
compatibility bridge.
+            python -m pip install tomli
+            python paimon-python/dev/install.py legacy
           else
-            python -m pip install --upgrade pip
-            pip install torch --index-url https://download.pytorch.org/whl/cpu
-            python -m pip install pyroaring readerwriterlock==1.0.9 
fsspec==2024.3.1 cachetools==5.3.3 ossfs==2023.12.0 ray==2.54.0 
fastavro==1.11.1 'isal>=1.8,<2' zstandard==0.24.0 polars==1.32.0 duckdb==1.3.2 
pylance==0.39.0 cramjam pytest~=7.0 py4j==0.10.9.9 requests 
parameterized==0.9.0 'daft>=0.7.6' 'datafusion>=54,<55' datasketches
-            if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 
12) else 1)"; then
-              python -m pip install pyarrow==24.0.0 numpy==2.4.6 pandas==2.3.3 
flake8==7.1.2
-            else
-              python -m pip install pyarrow==16.0.0 numpy==1.24.3 
pandas==2.0.3 flake8==4.0.1
-            fi
-            python -m pip install 'h5py>=3,<4' Pillow
-            python -c "import h5py; print('h5py', h5py.__version__)"
-            if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 
10) else 1)"; then
-              python -m pip install './paimon-python[rosbag]'
-              python -c "import rosbags; print('rosbags installed')"
-            fi
+            python -m pip install torch --index-url 
https://download.pytorch.org/whl/cpu
+            dependency_group=dev
             if [[ "${{ matrix.python-version }}" == "3.10" ]]; then
-              python -m pip install './paimon-python[lerobot,video]'
-              python -c "import datasets, lerobot; print('datasets', 
datasets.__version__, 'lerobot', lerobot.__version__)"
-            fi
-
-            # Required by SQL tests; native planner tests run against main 
below.
-            python -m pip install pypaimon-rust
-            if [[ "${{ matrix.python-version }}" == "3.11" ]]; then
-              # Run the RoboMIND pipeline tests with synthetic local HDF5 data.
-              python -m pip install "./paimon-python[sql]"
-            fi
-            python -m pip install 'lumina-data>=${{ env.LUMINA_DATA_VERSION 
}}' -i https://pypi.org/simple/
-            if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 
11) else 1)"; then
-              # Vortex requires pyarrow >= 17, while Pypaimon requires pyarrow 
< 20 and != 19.
-              python -m pip install vortex-data==0.70.0 pyarrow==18.1.0
-            fi
-            python -m pip install 'paimon-mosaic>=0.1.0'
-            if [[ "${{ matrix.python-version }}" == "3.11" ]]; then
-              python -m pip check
+              dependency_group=torch
             fi
+            python -m pip install -e ./paimon-python --group 
"./paimon-python/pyproject.toml:${dependency_group}"
           fi
-          python -m pip install 'oss2>=2.18,<3'
-          df -h
+          python -m pip check
 
       - name: Run lint-python.sh
         shell: bash
@@ -204,13 +173,7 @@ jobs:
       - name: Install Python test dependencies
         run: |
           python -m pip install torch --index-url 
https://download.pytorch.org/whl/cpu
-          python -m pip install "./paimon-python[sql,rosbag]" \
-            readerwriterlock==1.0.9 fsspec==2024.3.1 cachetools==5.3.3 
ossfs==2023.12.0 \
-            ray==2.54.0 fastavro==1.11.1 zstandard==0.24.0 polars==1.32.0 
duckdb==1.3.2 \
-            pylance==0.39.0 'pytest~=7.0' py4j==0.10.9.9 parameterized==0.9.0 \
-            'daft>=0.7.6' datasketches pyarrow==18.1.0 numpy==1.24.3 
pandas==2.0.3 \
-            'h5py>=3,<4' Pillow 'lumina-data>=${{ env.LUMINA_DATA_VERSION }}' \
-            vortex-data==0.70.0 'paimon-mosaic>=0.1.0'
+          python -m pip install -e ./paimon-python --group 
./paimon-python/pyproject.toml:dev
           python -m pip check
 
       - name: Verify native wheel provenance
@@ -262,21 +225,20 @@ jobs:
         run: python --version
 
       - name: Install Python dependencies
-        shell: bash
         run: |
-            python -m pip install --upgrade pip
-            pip install torch --index-url https://download.pytorch.org/whl/cpu
-            python -m pip install pyroaring readerwriterlock==1.0.9 
fsspec==2024.3.1 cachetools==5.3.3 ossfs==2023.12.0 ray==2.54.0 
fastavro==1.11.1 pyarrow==16.0.0 zstandard==0.24.0 polars==1.32.0 duckdb==1.3.2 
numpy==1.24.3 pandas==2.0.3 pylance==0.39.0 flake8==4.0.1 pytest~=7.0 
py4j==0.10.9.9 requests parameterized==0.9.0 datasketches 'datasets>=4,<4.1'
-            python -m pip install 'lumina-data>=${{ env.LUMINA_DATA_VERSION 
}}' -i https://pypi.org/simple/
+          python -m pip install --upgrade pip
+          python -m pip install torch --index-url 
https://download.pytorch.org/whl/cpu
+          python -m pip install -e ./paimon-python --group 
./paimon-python/pyproject.toml:torch
+          python -m pip check
       - name: Run lint-python.sh
         shell: bash
         run: |
           chmod +x paimon-python/dev/lint-python.sh
           ./paimon-python/dev/lint-python.sh -i pytest_torch
 
-  # One job: check dev/requirements.txt on each Python version in sequence, 
then Ray version test on 3.10.
+  # Check core installation across Python versions, then basic Ray 
compatibility on 3.10.
   compatibility:
-    name: Requirements and Ray compatibility
+    name: Package and Ray compatibility
     timeout-minutes: 60
     runs-on: ubuntu-latest
     steps:
@@ -289,71 +251,71 @@ jobs:
             build-essential git curl cmake \
             && sudo rm -rf /var/lib/apt/lists/*
 
-      - name: Verify dev/requirements.txt on Python 3.8
+      - name: Verify core installation on Python 3.8
         uses: actions/setup-python@v5
         with:
           python-version: '3.8'
-      - name: Resolve requirements (3.8)
+      - name: Install core package (3.8)
         run: |
           cd paimon-python && python -m pip install --upgrade pip
           TEMP_DIR=$(mktemp -d)
-          python -m pip install -r dev/requirements.txt --target "$TEMP_DIR" 
|| { rm -rf "$TEMP_DIR"; exit 1; }
-          rm -rf "$TEMP_DIR" && echo "✓ dev/requirements.txt OK on Python 3.8"
+          python -m pip install . --target "$TEMP_DIR" || { rm -rf 
"$TEMP_DIR"; exit 1; }
+          rm -rf "$TEMP_DIR" && echo "✓ Core package OK on Python 3.8"
 
-      - name: Verify dev/requirements.txt on Python 3.9
+      - name: Verify core installation on Python 3.9
         uses: actions/setup-python@v5
         with:
           python-version: '3.9'
-      - name: Resolve requirements (3.9)
+      - name: Install core package (3.9)
         run: |
           cd paimon-python && python -m pip install --upgrade pip
           TEMP_DIR=$(mktemp -d)
-          python -m pip install -r dev/requirements.txt --target "$TEMP_DIR" 
|| { rm -rf "$TEMP_DIR"; exit 1; }
-          rm -rf "$TEMP_DIR" && echo "✓ dev/requirements.txt OK on Python 3.9"
+          python -m pip install . --target "$TEMP_DIR" || { rm -rf 
"$TEMP_DIR"; exit 1; }
+          rm -rf "$TEMP_DIR" && echo "✓ Core package OK on Python 3.9"
 
-      - name: Verify dev/requirements.txt on Python 3.10
+      - name: Verify core installation on Python 3.10
         uses: actions/setup-python@v5
         with:
           python-version: '3.10'
-      - name: Resolve requirements (3.10)
+      - name: Install core package (3.10)
         run: |
           cd paimon-python && python -m pip install --upgrade pip
           TEMP_DIR=$(mktemp -d)
-          python -m pip install -r dev/requirements.txt --target "$TEMP_DIR" 
|| { rm -rf "$TEMP_DIR"; exit 1; }
-          rm -rf "$TEMP_DIR" && echo "✓ dev/requirements.txt OK on Python 3.10"
+          python -m pip install . --target "$TEMP_DIR" || { rm -rf 
"$TEMP_DIR"; exit 1; }
+          rm -rf "$TEMP_DIR" && echo "✓ Core package OK on Python 3.10"
 
-      - name: Verify dev/requirements.txt on Python 3.11
+      - name: Verify core installation on Python 3.11
         uses: actions/setup-python@v5
         with:
           python-version: '3.11'
-      - name: Resolve requirements (3.11)
+      - name: Install core package (3.11)
         run: |
           cd paimon-python && python -m pip install --upgrade pip
           TEMP_DIR=$(mktemp -d)
-          python -m pip install -r dev/requirements.txt --target "$TEMP_DIR" 
|| { rm -rf "$TEMP_DIR"; exit 1; }
-          rm -rf "$TEMP_DIR" && echo "✓ dev/requirements.txt OK on Python 3.11"
+          python -m pip install . --target "$TEMP_DIR" || { rm -rf 
"$TEMP_DIR"; exit 1; }
+          rm -rf "$TEMP_DIR" && echo "✓ Core package OK on Python 3.11"
 
-      - name: Verify dev/requirements.txt on Python 3.12
+      - name: Verify core installation on Python 3.12
         uses: actions/setup-python@v5
         with:
           python-version: '3.12'
-      - name: Resolve requirements (3.12)
+      - name: Install core package (3.12)
         run: |
           cd paimon-python && python -m pip install --upgrade pip
           TEMP_DIR=$(mktemp -d)
-          python -m pip install -r dev/requirements.txt --target "$TEMP_DIR" 
|| { rm -rf "$TEMP_DIR"; exit 1; }
-          rm -rf "$TEMP_DIR" && echo "✓ dev/requirements.txt OK on Python 3.12"
+          python -m pip install . --target "$TEMP_DIR" || { rm -rf 
"$TEMP_DIR"; exit 1; }
+          rm -rf "$TEMP_DIR" && echo "✓ Core package OK on Python 3.12"
 
-      - name: Verify dev/requirements.txt on Python 3.13
+      - name: Verify core installation on Python 3.13
         uses: actions/setup-python@v5
         with:
           python-version: '3.13'
-      - name: Resolve requirements (3.13)
+      - name: Install core package (3.13)
         run: |
           cd paimon-python && python -m pip install --upgrade pip
           TEMP_DIR=$(mktemp -d)
-          python -m pip install -r dev/requirements.txt --target "$TEMP_DIR" 
|| { rm -rf "$TEMP_DIR"; exit 1; }
-          rm -rf "$TEMP_DIR" && echo "✓ dev/requirements.txt OK on Python 3.13"
+          python -m pip install . --target "$TEMP_DIR" || { rm -rf 
"$TEMP_DIR"; exit 1; }
+          rm -rf "$TEMP_DIR" && echo "✓ Core package OK on Python 3.13"
 
       - name: Setup Python 3.10 for Ray test
         uses: actions/setup-python@v5
@@ -362,13 +324,10 @@ jobs:
       - name: Install base Python dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install torch --index-url https://download.pytorch.org/whl/cpu
-          python -m pip install --no-cache-dir \
-            pyroaring readerwriterlock==1.0.9 fsspec==2024.3.1 
cachetools==5.3.3 ossfs==2023.12.0 \
-            fastavro==1.11.1 pyarrow==16.0.0 zstandard==0.24.0 polars==1.32.0 
duckdb==1.3.2 \
-            numpy==1.24.3 pandas==2.0.3 cramjam pytest~=7.0 py4j==0.10.9.9 
requests \
-            parameterized==0.9.0 packaging datasketches
-          python -m pip install 'lumina-data>=${{ env.LUMINA_DATA_VERSION }}' 
-i https://pypi.org/simple/
+          python -m pip install torch --index-url 
https://download.pytorch.org/whl/cpu
+          # Use only test tools here: the dev group's Ray >=2.50 floor is for 
full-feature tests.
+          python -m pip install 
'./paimon-python[ray,oss,lumina,vindex,full-text,theta-sketch]' \
+            --group ./paimon-python/pyproject.toml:test
       - name: Test Ray version compatibility
         run: |
           cd paimon-python
diff --git a/.github/workflows/release-python-publish.yml 
b/.github/workflows/release-python-publish.yml
index a153ff2d4c..bb0fec4b70 100644
--- a/.github/workflows/release-python-publish.yml
+++ b/.github/workflows/release-python-publish.yml
@@ -61,7 +61,7 @@ jobs:
 
           source_version="$(
             sed -n 's/^VERSION = "\(.*\)"/\1/p' \
-              paimon-python/setup.py
+              paimon-python/pypaimon/_version.py
           )"
           release_version="${{ inputs.release_version }}"
           if [[ "${source_version}" != "${release_version}" ]]; then
diff --git a/.github/workflows/release-python.yml 
b/.github/workflows/release-python.yml
index c8d3ef32e2..f0bab24cbf 100644
--- a/.github/workflows/release-python.yml
+++ b/.github/workflows/release-python.yml
@@ -52,7 +52,7 @@ jobs:
         with:
           python-version: "3.11"
           cache: pip
-          cache-dependency-path: paimon-python/dev/requirements.txt
+          cache-dependency-path: paimon-python/pyproject.toml
 
       - name: Set package version
         id: package_version
@@ -62,7 +62,7 @@ jobs:
 
           source_version="$(
             sed -n 's/^VERSION = "\(.*\)"/\1/p' \
-              paimon-python/setup.py
+              paimon-python/pypaimon/_version.py
           )"
           release_version="${{ inputs.release_version }}"
           if [[ "${source_version}" != "${release_version}" ]]; then
@@ -74,19 +74,18 @@ jobs:
           if [[ "${GITHUB_REF}" == refs/tags/*-rc* ]]; then
             rc_number="${GITHUB_REF_NAME##*-rc}"
             package_version="${release_version}rc${rc_number}"
-            sed -i \
-              "s/^VERSION = \".*\"/VERSION = \"${package_version}\"/" \
-              paimon-python/setup.py
           fi
 
           echo "version=${package_version}" >> "${GITHUB_OUTPUT}"
+          echo "PYPAIMON_BUILD_VERSION=${package_version}" >> "${GITHUB_ENV}"
           echo "Building PyPaimon ${package_version}"
 
       - name: Build distributions
         working-directory: paimon-python
         run: |
-          python -m pip install --upgrade pip setuptools wheel twine
-          python setup.py sdist bdist_wheel
+          python -m pip install --upgrade pip
+          python -m pip install --group build
+          python -m build
           python -m twine check dist/*
 
       - name: Verify distribution contents
@@ -111,7 +110,8 @@ jobs:
           with tarfile.open(sdist, "r:gz") as archive:
               names = archive.getnames()
               prefix = "pypaimon-{}/".format(expected)
-              for required in ("LICENSE", "NOTICE", "README.md", "setup.py"):
+              for required in ("LICENSE", "NOTICE", "README.md", "setup.py", 
"pyproject.toml",
+                               "pypaimon/_version.py", 
"pypaimon/_full_version"):
                   if prefix + required not in names:
                       raise SystemExit("Missing {} from 
sdist".format(required))
               sample_data = "pypaimon/sample/data/data.jsonl"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index caaf86a89e..2316ae6968 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -81,7 +81,7 @@ jobs:
 
           python_release_version="$(
             sed -n 's/^VERSION = "\(.*\)"/\1/p' \
-              paimon-python/setup.py
+              paimon-python/pypaimon/_version.py
           )"
           if [[ "${python_release_version}" != "${release_version}" ]]; then
             echo "PyPaimon version ${python_release_version:-missing} does not 
match Paimon ${release_version}" >&2
diff --git a/.gitignore b/.gitignore
index 54381f82eb..9f1331ac3d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,6 +35,7 @@ metastore_db/
 
 ### Python ###
 *.pyc
+paimon-python/build/
 paimon-python/dist/
 paimon-python/*.egg-info/
 paimon-python/dev/log
diff --git a/docs/docs/project/creating-a-release.md 
b/docs/docs/project/creating-a-release.md
index c8c15c41e2..f42e2060c5 100644
--- a/docs/docs/project/creating-a-release.md
+++ b/docs/docs/project/creating-a-release.md
@@ -169,7 +169,7 @@ NEW_VERSION="${PAIMON_VERSION}" \
   ./tools/releasing/update_branch_version.sh
 ```
 
-Set `VERSION` in `paimon-python/setup.py` to the final
+Set `VERSION` in `paimon-python/pypaimon/_version.py` to the final
 `PAIMON_VERSION`, without `.dev` or an RC suffix. The release workflow
 derives the TestPyPI version by appending `rcRC_NUMBER`; the source candidate
 keeps the final version.
@@ -180,9 +180,9 @@ candidate contains no snapshot or development version:
 
 ```shell
 mvn -q -DforceStdout help:evaluate -Dexpression=project.version
-python3 paimon-python/setup.py --version
+python3 -c "import runpy; 
print(runpy.run_path('paimon-python/pypaimon/_version.py')['VERSION'])"
 rg --glob 'pom.xml' "<version>${PAIMON_VERSION}-SNAPSHOT</version>"
-rg '^VERSION = ".*\.dev' paimon-python/setup.py
+rg '^VERSION = ".*\.dev' paimon-python/pypaimon/_version.py
 git status --short
 ```
 
@@ -316,7 +316,8 @@ RELEASE_VERSION="${PAIMON_VERSION}" \
   ./tools/releasing/create_source_release.sh
 
 cd paimon-python
-python3 setup.py sdist
+python3 -m pip install --group build
+python3 -m build --sdist
 gpg --armor --detach-sig \
   "dist/pypaimon-${PAIMON_VERSION}.tar.gz"
 
diff --git a/docs/docs/project/releases.md b/docs/docs/project/releases.md
index 3e23b79fb7..5e72367d3f 100644
--- a/docs/docs/project/releases.md
+++ b/docs/docs/project/releases.md
@@ -65,7 +65,7 @@ PyPI package is built from the approved commit with the final 
version.
 ## Release model
 
 The combined Paimon and PyPaimon release uses one shared version number. The
-Maven project version and `paimon-python/setup.py` version must be equal.
+Maven project version and `paimon-python/pypaimon/_version.py` version must be 
equal.
 
 | Deliverable | Candidate | Published location |
 | --- | --- | --- |
diff --git a/docs/docs/project/verifying-a-release-candidate.md 
b/docs/docs/project/verifying-a-release-candidate.md
index f3cd609281..53c4d5455e 100644
--- a/docs/docs/project/verifying-a-release-candidate.md
+++ b/docs/docs/project/verifying-a-release-candidate.md
@@ -189,7 +189,7 @@ Check at least the following:
 - All bundled third-party material is compatible with the Apache License 2.0
   and is recorded in `LICENSE` or `NOTICE` where required.
 - Maven POMs use `PAIMON_VERSION` without `-SNAPSHOT`.
-- `paimon-python/setup.py` and PyPaimon package metadata use
+- `paimon-python/pypaimon/_version.py` and PyPaimon package metadata use
   `PAIMON_VERSION` without `.dev`.
 
 Extract the Paimon candidate:
@@ -324,10 +324,11 @@ First build the official PyPaimon source candidate in an 
isolated environment:
 ```shell
 python3 -m venv pypaimon-rc-venv
 . pypaimon-rc-venv/bin/activate
-python -m pip install --upgrade pip setuptools wheel
+python -m pip install --upgrade pip
 
 cd "pypaimon-${PAIMON_VERSION}"
-python setup.py sdist bdist_wheel
+python -m pip install --group build
+python -m build
 python -m pip install "dist/pypaimon-${PAIMON_VERSION}"*.whl
 python -c "import pypaimon; print('PyPaimon import OK')"
 cd ..
@@ -349,8 +350,7 @@ than assuming that a matrix from another release applies:
 ```shell
 (
   cd "paimon-${PAIMON_VERSION}/paimon-python"
-  python -m pip install -r dev/requirements.txt
-  python -m pip install -r dev/requirements-dev.txt
+  python -m pip install -e . --group dev
   python -m pytest pypaimon/tests -v
 )
 ```
@@ -358,6 +358,10 @@ than assuming that a matrix from another release applies:
 Some optional integration tests need additional services or packages. Record
 which tests ran, skipped, or failed.
 
+For Python 3.6/3.7, install a compatible `tomli`, run `python dev/install.py 
legacy`,
+and use `bash dev/lint-python.sh -i pytest` to select the supported core test 
subset.
+The complete dev group is intended for a modern Python environment.
+
 ## Verify the TestPyPI candidate
 
 Install the exact RC version in a new environment:
diff --git a/docs/docs/pypaimon/installation.md 
b/docs/docs/pypaimon/installation.md
index a4efd18b6b..d0a6647583 100644
--- a/docs/docs/pypaimon/installation.md
+++ b/docs/docs/pypaimon/installation.md
@@ -90,22 +90,24 @@ Python module with:
 python -m pip install ./paimon-python
 ```
 
-For local Python development, use an editable installation:
+For local Python development, use pip with dependency-group support to install
+the editable project and development tools together:
 
 ```shell
-python -m pip install -e './paimon-python'
+cd paimon-python
+python -m pip install -e . --group dev
 ```
 
-To produce a source archive:
+To produce a source archive and wheel, run from `paimon-python/`:
 
 ```shell
-cd paimon-python
-python setup.py sdist
+python -m pip install --group build
+python -m build
 ```
 
-Archives are written to `paimon-python/dist/`. Development builds can produce
-both a base development archive and a dated archive; select one archive when
-installing instead of passing `dist/*.tar.gz` to pip.
+The source archive and wheel are written to `dist/` with the same version.
+Development versions append the Git commit date, for example `2.2.dev20260927`.
+Install the wheel with `python -m pip install dist/*.whl`.
 
 ## Verify the installation
 
diff --git a/paimon-python/MANIFEST.in b/paimon-python/MANIFEST.in
index d644332dce..35a33f1e5f 100644
--- a/paimon-python/MANIFEST.in
+++ b/paimon-python/MANIFEST.in
@@ -17,7 +17,7 @@
 
################################################################################
 include LICENSE
 include NOTICE
-include dev/requirements.txt
+include pyproject.toml
 include README.md
 recursive-include pypaimon *.py
 recursive-include pypaimon/sample/data *.jsonl
diff --git a/paimon-python/README.md b/paimon-python/README.md
index 246af0693a..1b7c200b89 100644
--- a/paimon-python/README.md
+++ b/paimon-python/README.md
@@ -10,10 +10,25 @@ This PyPi package contains the Python APIs for using Paimon.
 
 Pypaimon requires Python 3.6+.
 
-# Dependencies
+# Build
+
+Run from `paimon-python/` with pip supporting dependency groups.
+
+Normal build:
+
+```shell
+python -m pip install --group build
+python -m build
+```
 
-The core dependencies are listed in `dev/requirements.txt`.
-The development dependencies are listed in `dev/requirements-dev.txt`.
+Development build (editable installation with development dependencies):
+
+```shell
+python -m pip install -e . --group dev
+python -m build
+```
+
+Both produce a source archive and wheel in `dist/`.
 
 # OSS metadata commits
 
@@ -89,22 +104,6 @@ All concurrent writers must use conditional creation. Older 
Python clients or
 other clients that overwrite snapshot objects can still overwrite a successful
 commit. This change does not add conditional writes for other object stores.
 
-# Build
-
-You can build the source package by executing the following command:
-
-```commandline
-python3 setup.py sdist
-```
-
-The package is under `dist/`. Then you can install the package by executing 
the following command:
-
-```commandline
-pip3 install dist/*.tar.gz
-```
-
-The command will install the package and core dependencies to your local 
Python environment.
-
 # Row ID column updates
 
 For a batch update of selected columns in a data-evolution table with row
diff --git a/paimon-python/conftest.py b/paimon-python/conftest.py
index ef2e3b5ec1..7aa11d1fc6 100644
--- a/paimon-python/conftest.py
+++ b/paimon-python/conftest.py
@@ -198,7 +198,8 @@ def enable_native_backends(request, monkeypatch):
     python_read = request.node.get_closest_marker("python_read") is not None
     python_write = request.node.get_closest_marker("python_write") is not None
     python_commit = request.node.get_closest_marker("python_commit") is not 
None
-    native_plan_test = request.path.name in (
+    # request.path requires pytest 7; Python 3.6 uses pytest 6.
+    native_plan_test = os.path.basename(request.node.location[0]) in (
         "native_plan_test.py", "native_plan_integration_test.py",
         "native_plan_capabilities_test.py")
     force_plan = _native_plan_enabled() and not python_plan and not 
native_plan_test
diff --git a/paimon-python/dev/check-licensing.sh 
b/paimon-python/dev/check-licensing.sh
index c867a0d7b6..19bb7f013b 100755
--- a/paimon-python/dev/check-licensing.sh
+++ b/paimon-python/dev/check-licensing.sh
@@ -48,7 +48,7 @@ if [ -z "${SOURCE_PACKAGE}" ]; then
     cd ${PROJECT_ROOT}
 
     # Sanity check to ensure that resolved paths are valid; a LICENSE file 
should always exist in project root
-    if [ ! -f ${PROJECT_ROOT}/setup.py ]; then
+    if [ ! -f ${PROJECT_ROOT}/pyproject.toml ]; then
         echo "Project root path ${PROJECT_ROOT} is not valid; script may be in 
the wrong directory."
         exit 1
     fi
diff --git a/paimon-python/dev/install.py b/paimon-python/dev/install.py
new file mode 100644
index 0000000000..d1100606ce
--- /dev/null
+++ b/paimon-python/dev/install.py
@@ -0,0 +1,71 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Install TOML dependency groups on old Python/pip without --group support.
+
+Modern development environments use ``pip install -e . --group dev`` directly.
+This bridge reads the same configuration for the Python 3.6/3.7 CI lanes.
+"""
+import argparse
+import os
+import re
+import subprocess
+import sys
+
+try:
+    import tomllib
+except ImportError:
+    import tomli as tomllib
+
+
+def _normalize(name):
+    return re.sub(r"[-_.]+", "-", name).lower()
+
+
+def resolve_group(groups, name, parents=()):
+    name = _normalize(name)
+    if name in parents:
+        raise ValueError("Cyclic dependency group: {}".format(name))
+    requirements = []
+    for item in groups[name]:
+        if isinstance(item, str):
+            requirements.append(item)
+        elif isinstance(item, dict) and set(item) == {"include-group"}:
+            requirements.extend(resolve_group(
+                groups, item["include-group"], parents + (name,)))
+        else:
+            raise ValueError("Invalid dependency group item: 
{!r}".format(item))
+    return requirements
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("group")
+    args = parser.parse_args()
+    root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    with open(os.path.join(root, "pyproject.toml"), "rb") as project_file:
+        raw_groups = tomllib.load(project_file)["dependency-groups"]
+    groups = {_normalize(name): value for name, value in raw_groups.items()}
+    if len(groups) != len(raw_groups):
+        raise ValueError("Duplicate normalized dependency group names")
+    subprocess.check_call(
+        [sys.executable, "-m", "pip", "install", root] +
+        resolve_group(groups, args.group))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/paimon-python/dev/lint-python.sh b/paimon-python/dev/lint-python.sh
index 1795379f17..86d77a6782 100755
--- a/paimon-python/dev/lint-python.sh
+++ b/paimon-python/dev/lint-python.sh
@@ -152,7 +152,7 @@ function license_check() {
 
 # Flake8 check
 function flake8_check() {
-    local PRUNE_PATHS="\( -path ./dev -o -path ./.tox -o -path ./.venv"
+    local PRUNE_PATHS="\( -path ./dev -o -path ./.tox -o -path ./.venv -o 
-path ./build -o -path ./dist"
     if python -c "import sys; sys.exit(0 if sys.version_info < (3, 10) else 
1)" 2>/dev/null; then
         PRUNE_PATHS="$PRUNE_PATHS -o -path ./pypaimon/daft -o -path 
./pypaimon/tests/daft"
     fi
@@ -364,9 +364,5 @@ done
 # collect checks according to the options
 collect_checks
 
-if python -c "import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1)"; 
then
-    python -m pip install 'paimon-ftindex==0.1.0' || exit 1
-fi
-
 # run checks
 check_stage
diff --git a/paimon-python/dev/requirements-dev.txt 
b/paimon-python/dev/requirements-dev.txt
deleted file mode 100644
index 711b0ee62a..0000000000
--- a/paimon-python/dev/requirements-dev.txt
+++ /dev/null
@@ -1,41 +0,0 @@
-################################################################################
-#  Licensed to the Apache Software Foundation (ASF) under one
-#  or more contributor license agreements.  See the NOTICE file
-#  distributed with this work for additional information
-#  regarding copyright ownership.  The ASF licenses this file
-#  to you under the Apache License, Version 2.0 (the
-#  "License"); you may not use this file except in compliance
-#  with the License.  You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-# Core dependencies for pypaimon are in requirements.txt
-
-# Test dependencies for pypaimon are as follows
-duckdb==1.3.2
-flake8==4.0.1
-oss2>=2.18,<3
-pytest~=7.0
-# HDF5 ingestion tests run in the supported h5py wheel lanes (Python 3.8+).
-h5py>=3,<4; python_version >= "3.8"
-# merge_into needs Dataset.join (added in Ray 2.50). Python 3.8 has no 2.50 
wheel.
-ray>=2.10.0; python_version < "3.9"
-ray>=2.50.0; python_version >= "3.9"
-requests
-parameterized
-# Vortex 0.71.0 regresses native predicate pushdown on single-row files.
-vortex-data==0.70.0; python_version >= "3.11"
-# merge_into condition expressions (optional, for condition tests)
-datafusion>=52; python_version >= "3.10"
-# Lumina vector search (optional, for lumina index tests)
-lumina-data>=0.1.0
-# paimon-vindex vector search (optional, for vindex index tests)
-paimon-vindex==0.5.0; python_version >= "3.9"
-# paimon-ftindex full-text search (optional, for full-text index tests)
-paimon-ftindex==0.1.0; python_version >= "3.8"
diff --git a/paimon-python/dev/requirements.txt 
b/paimon-python/dev/requirements.txt
deleted file mode 100644
index dc9b4b4e91..0000000000
--- a/paimon-python/dev/requirements.txt
+++ /dev/null
@@ -1,44 +0,0 @@
-################################################################################
-#  Licensed to the Apache Software Foundation (ASF) under one
-#  or more contributor license agreements.  See the NOTICE file
-#  distributed with this work for additional information
-#  regarding copyright ownership.  The ASF licenses this file
-#  to you under the Apache License, Version 2.0 (the
-#  "License"); you may not use this file except in compliance
-#  with the License.  You may obtain a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing, software
-#  distributed under the License is distributed on an "AS IS" BASIS,
-#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#  See the License for the specific language governing permissions and
-# limitations under the License.
-################################################################################
-cachetools>=4.2,<6; python_version=="3.6"
-cachetools>=5,<6; python_version>"3.6"
-dataclasses>=0.8; python_version < "3.7"
-fastavro>=1.4,<2
-isal>=1.8,<2; python_version >= "3.9" and (platform_machine == "x86_64" or 
platform_machine == "AMD64" or platform_machine == "aarch64" or 
platform_machine == "arm64")
-fsspec>=2021.10,<2026; python_version<"3.8"
-fsspec>=2023,<2026; python_version>="3.8"
-packaging>=21,<26
-pandas>=1.1,<2; python_version < "3.7"
-pandas>=1.3,<3; python_version >= "3.7" and python_version < "3.9"
-pandas>=1.5,<3; python_version >= "3.9"
-polars>=0.9,<1; python_version<"3.8"
-polars>=1,<2; python_version=="3.8"
-polars>=1.32,<2; python_version>="3.9"
-pyarrow>=6,<7; python_version < "3.7"
-pyarrow>=7,<13; python_version >= "3.7" and python_version < "3.8"
-pyarrow>=16,!=19.0.0,<20; python_version >= "3.8"
-pyroaring<=0.3.3; python_version < "3.7"
-pyroaring<=0.4.5; python_version == "3.7"
-pyroaring>=1.0.0; python_version >= "3.8"
-readerwriterlock>=1,<2
-requests>=2.21.0,<3
-urllib3>=1.26,<3
-zstandard>=0.19,<1
-backports.zstd>=1.0.0,<1.4.0; python_version >= "3.9" and python_version < 
"3.14"
-cramjam>=1.3.0,<3; python_version>="3.7"
-pyyaml>=5.4,<7
diff --git a/paimon-python/dev/run_mixed_tests.sh 
b/paimon-python/dev/run_mixed_tests.sh
index fc84202a0d..84a54e21db 100755
--- a/paimon-python/dev/run_mixed_tests.sh
+++ b/paimon-python/dev/run_mixed_tests.sh
@@ -660,29 +660,8 @@ ensure_paimon_vindex() {
         return 0
     fi
 
-    echo "Installing Python paimon-vindex dependency..."
-    if python -m pip install 'paimon-vindex==0.5.0'; then
-        return 0
-    fi
-
-    echo -e "${YELLOW}Direct pip install failed; installing paimon-vindex into 
a temporary target directory...${NC}"
-    local target_dir="${TMPDIR:-/tmp}/paimon-vindex-site"
-    rm -rf "$target_dir"
-    if python -m pip install --target "$target_dir" 'paimon-vindex==0.5.0'; 
then
-        export PYTHONPATH="$target_dir:${PYTHONPATH:-}"
-        return 0
-    fi
-
-    if python -c "import numpy" >/dev/null 2>&1; then
-        echo -e "${YELLOW}Dependency install failed but numpy is already 
available; retrying paimon-vindex without dependencies...${NC}"
-        rm -rf "$target_dir"
-        if python -m pip install --target "$target_dir" --no-deps 
'paimon-vindex==0.5.0'; then
-            export PYTHONPATH="$target_dir:${PYTHONPATH:-}"
-            return 0
-        fi
-    fi
-
-    echo -e "${RED}✗ Failed to install paimon-vindex${NC}"
+    echo -e "${RED}Missing paimon-vindex; install the Python dev dependency 
group first.${NC}"
+    echo "From paimon-python: python -m pip install -e . --group dev"
     return 1
 }
 
diff --git a/paimon-python/dev/test_packaging.py 
b/paimon-python/dev/test_packaging.py
new file mode 100644
index 0000000000..e9d0e5ea52
--- /dev/null
+++ b/paimon-python/dev/test_packaging.py
@@ -0,0 +1,212 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Exercise actual distributions without importing runtime/native dependencies.
+
+Run with ``python dev/test_packaging.py`` after installing the build tools.
+The same suite runs on Python 3.6 and modern Python in CI.
+"""
+import email
+import os
+from pathlib import Path
+import runpy
+import shutil
+import subprocess
+import sys
+import tarfile
+import tempfile
+import unittest
+import zipfile
+
+from packaging.markers import default_environment
+from packaging.requirements import Requirement
+from packaging.utils import canonicalize_name
+from packaging.version import Version
+
+try:
+    import tomllib
+except ImportError:
+    import tomli as tomllib
+
+
+def command(args, cwd, env=None):
+    result = subprocess.run(args, cwd=str(cwd), env=env,
+                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
+    if result.returncode:
+        raise AssertionError(result.stdout.decode("utf-8", 
errors="replace")[-8000:])
+    return result.stdout.decode("utf-8")
+
+
+def wheel_contents(path):
+    with zipfile.ZipFile(str(path)) as archive:
+        names = archive.namelist()
+        metadata = email.message_from_bytes(archive.read(next(
+            name for name in names if name.endswith(".dist-info/METADATA"))))
+        full_version = archive.read("pypaimon/_full_version").decode().strip()
+    return names, metadata, full_version
+
+
+class PackagingTest(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls):
+        cls.temp = tempfile.TemporaryDirectory(prefix="pypaimon-packaging-")
+        cls.root = Path(cls.temp.name)
+        cls.source = cls.root / "source"
+        project = Path(__file__).resolve().parent.parent
+        shutil.copytree(str(project), str(cls.source), 
ignore=shutil.ignore_patterns(
+            "build", "dist", "*.egg-info", "__pycache__", ".pytest_cache", 
".venv"))
+        cls.env = os.environ.copy()
+        for key in ("GIT_DIR", "GIT_WORK_TREE", "PYPAIMON_BUILD_VERSION"):
+            cls.env.pop(key, None)
+        cls.env.update(GIT_AUTHOR_DATE="2020-01-02T03:04:05+00:00",
+                       GIT_COMMITTER_DATE="2020-01-02T03:04:05+00:00")
+        cls.commit = cls.init_git(cls.source)
+        base = runpy.run_path(str(cls.source / 
"pypaimon/_version.py"))["VERSION"]
+        cls.version = str(Version(base + "20200102" if base.endswith(".dev") 
else base))
+        with (cls.source / "pyproject.toml").open("rb") as config:
+            cls.config = tomllib.load(config)
+        cls.dist = cls.root / "custom-output"
+        cls.build(cls.source, cls.dist)
+        cls.wheel = next(cls.dist.glob("*.whl"))
+        cls.sdist = next(cls.dist.glob("*.tar.gz"))
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.temp.cleanup()
+
+    @classmethod
+    def init_git(cls, root):
+        for args in (["init", "-q"], ["config", "user.name", "Packaging Test"],
+                     ["config", "user.email", "[email protected]"], ["add", 
"."],
+                     ["commit", "-qm", "test source"]):
+            command(["git"] + args, root, cls.env)
+        return command(["git", "rev-parse", "HEAD"], root, cls.env).strip()
+
+    @classmethod
+    def build(cls, source, output, extra=(), env=None):
+        command([sys.executable, "-m", "build", "--no-isolation", "--outdir",
+                 str(output)] + list(extra), source, env or cls.env)
+
+    def test_sdist_and_wheel_share_version_and_commit(self):
+        self.assertEqual(2, len(list(self.dist.iterdir())))
+        self.assertEqual("pypaimon-{}.tar.gz".format(self.version), 
self.sdist.name)
+        self.assertEqual("pypaimon-{}-py3-none-any.whl".format(self.version), 
self.wheel.name)
+        _, metadata, full_version = wheel_contents(self.wheel)
+        self.assertEqual(self.version, metadata["Version"])
+        self.assertEqual("python-{}-{}".format(self.version, self.commit), 
full_version)
+        with tarfile.open(str(self.sdist)) as archive:
+            prefix = "pypaimon-{}/".format(self.version)
+            self.assertEqual(full_version, archive.extractfile(
+                prefix + "pypaimon/_full_version").read().decode().strip())
+
+    def test_distribution_contents(self):
+        names, metadata, _ = wheel_contents(self.wheel)
+        self.assertEqual(">=3.6", metadata["Requires-Python"])
+        self.assertFalse(any(name.startswith(("pypaimon/tests/", 
"pypaimon/acceptance/"))
+                             for name in names))
+        self.assertFalse(any(name.endswith((".so", ".pyd", ".jar")) for name 
in names))
+        for name in ("pypaimon/sample/data/data.jsonl", "pypaimon/_version.py",
+                     "pypaimon/benchmark/act/default_experiment.json"):
+            self.assertIn(name, names)
+        for suffix in ("/LICENSE", "/NOTICE"):
+            self.assertTrue(any(".dist-info/" in name and name.endswith(suffix)
+                                for name in names))
+        with tarfile.open(str(self.sdist)) as archive:
+            names = archive.getnames()
+            prefix = "pypaimon-{}/".format(self.version)
+            for name in ("pyproject.toml", "setup.py", "LICENSE", "NOTICE", 
"README.md"):
+                self.assertIn(prefix + name, names)
+            self.assertFalse(any("requirements.txt" in name or 
"requirements-dev.txt" in name
+                                 or name.startswith((prefix + 
"pypaimon/tests/",
+                                                     prefix + 
"pypaimon/acceptance/"))
+                                 for name in names))
+
+    @staticmethod
+    def active_requirements(values, environment):
+        result = set()
+        for value in values:
+            requirement = Requirement(value)
+            if requirement.marker is None or 
requirement.marker.evaluate(environment):
+                result.add((canonicalize_name(requirement.name), 
str(requirement.specifier)))
+        return result
+
+    def test_metadata_preserves_core_and_extra_conditions(self):
+        _, metadata, _ = wheel_contents(self.wheel)
+        project = self.config["project"]
+        self.assertEqual(set(project["optional-dependencies"]),
+                         set(metadata.get_all("Provides-Extra")))
+        for python_version in ("3.6", "3.7", "3.8", "3.11", "3.13"):
+            for system in ("Linux", "Windows"):
+                for extra in [""] + list(project["optional-dependencies"]):
+                    with self.subTest(python=python_version, system=system, 
extra=extra):
+                        environment = default_environment()
+                        environment.update(python_version=python_version,
+                                           python_full_version=python_version 
+ ".0",
+                                           platform_system=system, extra=extra)
+                        expected = project["dependencies"] + project[
+                            "optional-dependencies"].get(extra, [])
+                        self.assertEqual(self.active_requirements(expected, 
environment),
+                                         self.active_requirements(
+                                             
metadata.get_all("Requires-Dist"), environment))
+
+    def test_dev_group_reuses_product_extras(self):
+        resolve = runpy.run_path(str(self.source / 
"dev/install.py"))["resolve_group"]
+        requirements = [Requirement(value) for value in resolve(
+            self.config["dependency-groups"], "dev")]
+        project = next(requirement for requirement in requirements if 
requirement.name == "pypaimon")
+        self.assertTrue({"ray", "sql", "vindex", 
"full-text"}.issubset(project.extras))
+        # Product constraints belong to extras, not a second developer copy.
+        names = {requirement.name for requirement in requirements}
+        self.assertFalse(names.intersection({"datafusion", "paimon-vindex", 
"requests", "h5py"}))
+
+    def test_sdist_rebuild_preserves_provenance_in_another_git_repo(self):
+        outer = self.root / "downstream"
+        outer.mkdir()
+        (outer / "README").write_text("Unrelated downstream repository")
+        downstream_commit = self.init_git(outer)
+        self.assertNotEqual(self.commit, downstream_commit)
+        with tarfile.open(str(self.sdist)) as archive:
+            # This archive was just built from our test source.
+            archive.extractall(str(outer))
+        output = self.root / "rebuilt"
+        self.build(outer / "pypaimon-{}".format(self.version), output, 
extra=("--wheel",))
+        _, metadata, full_version = wheel_contents(next(output.glob("*.whl")))
+        self.assertEqual(self.version, metadata["Version"])
+        self.assertEqual("python-{}-{}".format(self.version, self.commit), 
full_version)
+
+    def test_release_override_applies_to_both_formats(self):
+        env = dict(self.env, PYPAIMON_BUILD_VERSION="2.2rc1")
+        output = self.root / "release-candidate"
+        self.build(self.source, output, env=env)
+        self.assertEqual({"pypaimon-2.2rc1.tar.gz", 
"pypaimon-2.2rc1-py3-none-any.whl"},
+                         {path.name for path in output.iterdir()})
+        _, metadata, full_version = wheel_contents(next(output.glob("*.whl")))
+        self.assertEqual("2.2rc1", metadata["Version"])
+        self.assertEqual("python-2.2rc1-{}".format(self.commit), full_version)
+        self.assertEqual("", command(["git", "diff", "--", 
"pypaimon/_version.py"],
+                                     self.source, self.env))
+
+    def test_invalid_release_override_fails(self):
+        env = dict(self.env, PYPAIMON_BUILD_VERSION="not a version")
+        result = subprocess.run([sys.executable, "-m", "build", 
"--no-isolation", "--sdist"],
+                                cwd=str(self.source), env=env,
+                                stdout=subprocess.PIPE, 
stderr=subprocess.STDOUT)
+        self.assertNotEqual(0, result.returncode)
+
+
+if __name__ == "__main__":
+    unittest.main(verbosity=2)
diff --git a/paimon-python/MANIFEST.in b/paimon-python/pypaimon/_version.py
similarity index 77%
copy from paimon-python/MANIFEST.in
copy to paimon-python/pypaimon/_version.py
index d644332dce..7948185ec7 100644
--- a/paimon-python/MANIFEST.in
+++ b/paimon-python/pypaimon/_version.py
@@ -1,4 +1,4 @@
-################################################################################
+##########################################################################
 #  Licensed to the Apache Software Foundation (ASF) under one
 #  or more contributor license agreements.  See the NOTICE file
 #  distributed with this work for additional information
@@ -14,12 +14,6 @@
 #  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 #  See the License for the specific language governing permissions and
 # limitations under the License.
-################################################################################
-include LICENSE
-include NOTICE
-include dev/requirements.txt
-include README.md
-recursive-include pypaimon *.py
-recursive-include pypaimon/sample/data *.jsonl
-recursive-exclude pypaimon/tests *
-recursive-exclude pypaimon/acceptance *
+##########################################################################
+# Base version; development distributions append the Git commit date.
+VERSION = "2.2.dev"
diff --git a/paimon-python/pypaimon/build_info.py 
b/paimon-python/pypaimon/build_info.py
index 22649caf77..6e8cd4cc41 100644
--- a/paimon-python/pypaimon/build_info.py
+++ b/paimon-python/pypaimon/build_info.py
@@ -16,12 +16,12 @@
 # under the License.
 
 import os
-import re
+import runpy
 import subprocess
 
 _UNKNOWN = "UNKNOWN"
 _FULL_VERSION_FILE = os.path.join(os.path.dirname(__file__), "_full_version")
-_SETUP_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 
"setup.py")
+_VERSION_FILE = os.path.join(os.path.dirname(__file__), "_version.py")
 
 
 def _repository_root():
@@ -33,36 +33,33 @@ def _repository_root():
     return python_root
 
 
-def git_commit_id():
-    """Return the current Paimon Git revision without discovering an outer 
repo."""
+def _git_output(args):
     repository_root = _repository_root()
     env = os.environ.copy()
     env["GIT_CEILING_DIRECTORIES"] = os.path.dirname(repository_root)
     try:
         return subprocess.check_output(
-            ["git", "-C", repository_root, "rev-parse", "HEAD"],
+            ["git", "-C", repository_root] + args,
             stderr=subprocess.DEVNULL,
             env=env,
         ).decode("utf-8").strip()
     except Exception:
-        return _UNKNOWN
+        return None
+
+
+def git_commit_id():
+    """Return the current Paimon Git revision without discovering an outer 
repo."""
+    return _git_output(["rev-parse", "HEAD"]) or _UNKNOWN
 
 
 def _source_version():
     try:
-        with open(_SETUP_FILE, "r") as setup_file:
-            match = re.search(
-                r'^VERSION = ["\']([^"\']+)["\']$',
-                setup_file.read(),
-                re.MULTILINE,
-            )
-            return None if match is None else match.group(1)
+        return runpy.run_path(_VERSION_FILE)["VERSION"]
     except OSError:
         return None
 
 
-def _load_full_version():
-    """Return the embedded full version, or derive it from the checkout."""
+def _embedded_full_version():
     try:
         with open(_FULL_VERSION_FILE, "r") as full_version_file:
             value = full_version_file.read().strip()
@@ -70,7 +67,28 @@ def _load_full_version():
                 return value
     except OSError:
         pass
+    return None
+
+
+def package_version():
+    """Resolve one version for metadata, sdist and wheel, even outside Git."""
+    embedded = _embedded_full_version()
+    if embedded:
+        return embedded[len("python-"):].rsplit("-", 1)[0]
     version = _source_version()
+    if version and version.endswith(".dev"):
+        # Commit date, rather than build date, keeps source rebuilds stable.
+        date = _git_output(["log", "-1", "--format=%cd", 
"--date=format:%Y%m%d"])
+        return version + (date or "0")
+    return version
+
+
+def _load_full_version():
+    """Return the embedded full version, or derive it from the checkout."""
+    embedded = _embedded_full_version()
+    if embedded:
+        return embedded
+    version = package_version()
     return (
         _UNKNOWN
         if version is None
diff --git a/paimon-python/pypaimon/tests/build_info_test.py 
b/paimon-python/pypaimon/tests/build_info_test.py
index a3dc11627e..b7a08aea69 100644
--- a/paimon-python/pypaimon/tests/build_info_test.py
+++ b/paimon-python/pypaimon/tests/build_info_test.py
@@ -34,7 +34,7 @@ class BuildInfoTest(unittest.TestCase):
         self.assertRegex(
             build_info.full_version(),
             r"^python-{}-(UNKNOWN|[0-9a-f]{{40}})$".format(
-                re.escape(build_info._source_version())),
+                re.escape(build_info.package_version())),
         )
 
     def test_embedded_full_version(self):
@@ -106,9 +106,10 @@ class BuildInfoTest(unittest.TestCase):
             embedded_file = os.path.join(extracted, "pypaimon", 
"_full_version")
             with open(embedded_file, "r") as full_version_file:
                 embedded = full_version_file.read().strip()
+            self.assertTrue(embedded.endswith("-" + upstream_commit))
             self.assertEqual(
-                "python-{}-{}".format(build_info._source_version(), 
upstream_commit),
-                embedded,
+                "pypaimon-{}".format(embedded[len("python-"):].rsplit("-", 
1)[0]),
+                top_level,
             )
 
             downstream_commit = self._init_git_repository(extracted, 
"downstream")
diff --git a/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py 
b/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
index f43c354b6e..f30bd443ca 100644
--- a/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
+++ b/paimon-python/pypaimon/tests/ray_data_evolution_merge_into_test.py
@@ -68,7 +68,8 @@ class RayDataEvolutionMergeIntoTest(unittest.TestCase):
         cls.catalog = CatalogFactory.create(cls.catalog_options)
         cls.catalog.create_database('default', True)
         if not ray.is_initialized():
-            ray.init(ignore_reinit_error=True, num_cpus=2)
+            # Nested joins and the final shuffle retain actors while 
scheduling tasks.
+            ray.init(ignore_reinit_error=True, num_cpus=4)
 
     @classmethod
     def tearDownClass(cls):
diff --git a/paimon-python/pypaimon/tests/rest/rest_read_write_test.py 
b/paimon-python/pypaimon/tests/rest/rest_read_write_test.py
index 4c9a9fd066..eb5d4b4594 100644
--- a/paimon-python/pypaimon/tests/rest/rest_read_write_test.py
+++ b/paimon-python/pypaimon/tests/rest/rest_read_write_test.py
@@ -546,10 +546,8 @@ class RESTTableReadWriteTest(RESTBaseTest):
         self.assertIsNotNone(ray_dataset, "Ray dataset should not be None")
         self.assertEqual(ray_dataset.count(), 8, "Should have 8 rows")
 
-        df = ray_dataset.to_pandas()
-        expect = pd.DataFrame(self.raw_data)
-        
pd.testing.assert_frame_equal(df.sort_values(by='user_id').reset_index(drop=True),
-                                      
expect.sort_values(by='user_id').reset_index(drop=True))
+        actual = pa.concat_tables(ray.get(ray_dataset.to_arrow_refs()))
+        self.assertEqual(actual.sort_by('user_id'), 
self.expected.sort_by('user_id'))
 
     def test_ray_data_write_and_read(self):
         if not ray.is_initialized():
@@ -587,15 +585,8 @@ class RESTTableReadWriteTest(RESTBaseTest):
         self.assertIsNotNone(ray_dataset, "Ray dataset should not be None")
         self.assertEqual(ray_dataset.count(), 3, "Should have 3 rows")
 
-        df = ray_dataset.to_pandas()
-        expected_df = pd.DataFrame({
-            'id': [1, 2, 3],
-            'name': ['Alice', 'Bob', 'Charlie'],
-            'value': [100, 200, 300],
-        })
-        expected_df['id'] = expected_df['id'].astype('int32')
-        
pd.testing.assert_frame_equal(df.sort_values(by='id').reset_index(drop=True),
-                                      
expected_df.sort_values(by='id').reset_index(drop=True))
+        actual = pa.concat_tables(ray.get(ray_dataset.to_arrow_refs()))
+        self.assertEqual(actual.sort_by('id'), test_data.sort_by('id'))
 
     def test_write_wide_table_large_data(self):
         logging.basicConfig(level=logging.INFO)
diff --git a/paimon-python/pyproject.toml b/paimon-python/pyproject.toml
new file mode 100644
index 0000000000..d39be4f839
--- /dev/null
+++ b/paimon-python/pyproject.toml
@@ -0,0 +1,211 @@
+################################################################################
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+# limitations under the License.
+################################################################################
+
+[build-system]
+requires = [
+    "setuptools>=40.8,<60; python_version < '3.7'",
+    "setuptools>=61; python_version >= '3.7'",
+    "wheel",
+    "packaging>=21",
+    "tomli>=1.2.3; python_version < '3.11'",
+]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "pypaimon"
+dynamic = ["version"]
+description = "Apache Paimon Python API"
+readme = "README.md"
+# Keep the table form for setuptools versions supporting Python 3.7/3.8.
+license = {text = "Apache-2.0"}
+authors = [{name = "Apache Software Foundation", email = 
"[email protected]"}]
+requires-python = ">=3.6"
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Programming Language :: Python :: 3.6",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+    "cachetools>=4.2,<6; python_version == '3.6'",
+    "cachetools>=5,<6; python_version > '3.6'",
+    "dataclasses>=0.8; python_version < '3.7'",
+    "fastavro>=1.4,<2",
+    "isal>=1.8,<2; python_version >= '3.9' and (platform_machine == 'x86_64' 
or platform_machine == 'AMD64' or platform_machine == 'aarch64' or 
platform_machine == 'arm64')",
+    "fsspec>=2021.10,<2026; python_version < '3.8'",
+    "fsspec>=2023,<2026; python_version >= '3.8'",
+    "packaging>=21",
+    "pandas>=1.1,<2; python_version < '3.7'",
+    "pandas>=1.3,<3; python_version >= '3.7' and python_version < '3.9'",
+    "pandas>=1.5,<3; python_version >= '3.9'",
+    "polars>=0.9,<1; python_version < '3.8'",
+    "polars>=1,<2; python_version == '3.8'",
+    "polars>=1.32,<2; python_version >= '3.9'",
+    "pyarrow>=6,<7; python_version < '3.7'",
+    "pyarrow>=7,<13; python_version >= '3.7' and python_version < '3.8'",
+    "pyarrow>=16,!=19.0.0,<20; python_version >= '3.8'",
+    "pyroaring<=0.3.3; python_version < '3.7'",
+    "pyroaring<=0.4.5; python_version == '3.7'",
+    "pyroaring>=1.0.0; python_version >= '3.8'",
+    "readerwriterlock>=1,<2",
+    "requests>=2.21.0,<3",
+    "urllib3>=1.26,<3",
+    "zstandard>=0.19,<1",
+    "backports.zstd>=1.0.0,<1.4.0; python_version >= '3.9' and python_version 
< '3.14'",
+    "cramjam>=1.3.0,<3; python_version >= '3.7'",
+    "pyyaml>=5.4,<7",
+]
+
+[project.urls]
+Homepage = "https://paimon.apache.org";
+Documentation = "https://paimon.apache.org/docs/master/pypaimon/python-api/";
+
+[project.scripts]
+paimon = "pypaimon.cli:main"
+
+[project.optional-dependencies]
+video = [
+    "av>=12,<19; python_version >= '3.10'",
+    "Pillow; python_version >= '3.10'",
+]
+hdf5 = [
+    "h5py>=3,<4; python_version >= '3.8'",
+]
+rosbag = [
+    "rosbags>=0.11.5,<0.12; python_version >= '3.10'",
+]
+# datasets 4.1+ may require PyArrow 21+, outside the supported core range.
+# Pandas 2.2.2+ supports NumPy 2 selected by the media dependencies.
+lerobot = [
+    "datasets>=4,<4.1; python_version >= '3.10'",
+    "pandas>=2.2.2,<3; python_version >= '3.10'",
+    "torch>=2.3; python_version >= '3.10'",
+    "lerobot>=0.4.4,<0.5; python_version >= '3.10'",
+]
+ray = [
+    "ray>=2.10,<3; python_version >= '3.8'",
+]
+torch = [
+    "torch",
+    "Pillow",
+]
+# ACT uses the same LeRobot stack; do not maintain its versions twice.
+act = [
+    "pypaimon[lerobot]; python_version >= '3.10'",
+    "Pillow; python_version >= '3.10'",
+]
+daft = [
+    "daft>=0.7.6; python_version >= '3.10'",
+]
+oss = [
+    "oss2>=2.18,<3",
+    "ossfs>=2021.8; python_version < '3.8'",
+    "ossfs>=2023; python_version >= '3.8'",
+]
+jindo = [
+    "oss2>=2.18,<3",
+    "pyjindosdk>=6.10.4",
+]
+lance = [
+    "pylance>=0.20,<1; python_version >= '3.9'",
+    "pylance>=0.10,<1; python_version >= '3.8' and python_version < '3.9'",
+]
+# Vortex 0.71 regresses native predicate pushdown on single-row files.
+vortex = [
+    "vortex-data==0.70.0; python_version >= '3.11'",
+]
+mosaic = [
+    "paimon-mosaic>=0.1.0; python_version >= '3.9'",
+]
+lumina = [
+    "lumina-data>=0.1.0",
+]
+vindex = [
+    "paimon-vindex==0.5.0; python_version >= '3.9'",
+]
+full-text = [
+    "paimon-ftindex==0.1.0; python_version >= '3.8'",
+]
+theta-sketch = [
+    "datasketches>=4,<5; python_version < '3.9'",
+    "datasketches>=5,<6; python_version >= '3.9'",
+]
+hll-sketch = [
+    "datasketches>=4,<5; python_version < '3.9'",
+    "datasketches>=5,<6; python_version >= '3.9'",
+]
+datafusion = [
+    "datafusion>=54,<55; python_version >= '3.10'",
+]
+sql = [
+    "pypaimon[datafusion]; python_version >= '3.10'",
+    "pypaimon-rust>=0.3.0; python_version >= '3.10'",
+]
+hdfs = [
+    "hdfs-native>=0.13,<1; python_version >= '3.10' and platform_system != 
'Windows'",
+]
+
+[dependency-groups]
+# Tools are separated from optional integrations so old-Python core tests and
+# targeted compatibility jobs do not install unrelated native dependencies.
+test = [
+    "pytest>=6.2,<7; python_version < '3.7'",
+    "pytest>=7,<8; python_version >= '3.7'",
+    "parameterized>=0.8,<0.9; python_version < '3.7'",
+    "parameterized>=0.9,<1; python_version >= '3.7'",
+    "py4j==0.10.9.9",
+]
+lint = [
+    "flake8>=4,<5; python_version < '3.8'",
+    "flake8>=7,<8; python_version >= '3.8'",
+]
+dev = [
+    {include-group = "test"},
+    {include-group = "lint"},
+    {include-group = "build"},
+    
"pypaimon[oss,lance,ray,daft,sql,hdf5,rosbag,video,vortex,mosaic,lumina,vindex,full-text,theta-sketch,hll-sketch]",
+    "duckdb>=1.3,<1.4; python_version >= '3.9'",
+    # merge_into / row-id operations require Dataset.join; basic Ray I/O does 
not.
+    "ray>=2.50; python_version >= '3.9'",
+]
+torch = [
+    {include-group = "dev"},
+    "pypaimon[torch,lerobot]",
+]
+legacy = [
+    {include-group = "test"},
+    {include-group = "lint"},
+    "pypaimon[oss,lumina,theta-sketch,hll-sketch]",
+]
+build = ["build", "twine"]
+
+[tool.setuptools]
+include-package-data = true
+license-files = ["LICENSE", "NOTICE"]
+
+[tool.setuptools.packages.find]
+include = ["pypaimon*"]
+exclude = ["pypaimon.tests*", "pypaimon.acceptance*"]
+
+[tool.setuptools.package-data]
+pypaimon = ["_full_version", "benchmark/act/default_experiment.json"]
diff --git a/paimon-python/setup.py b/paimon-python/setup.py
index 47e600343e..e673fe50bf 100644
--- a/paimon-python/setup.py
+++ b/paimon-python/setup.py
@@ -15,68 +15,29 @@
 #  See the License for the specific language governing permissions and
 # limitations under the License.
 ##########################################################################
-import atexit
+"""Setuptools hooks and the Python 3.6 bridge for pyproject.toml metadata."""
 import os
-import shutil
-import subprocess
+import runpy
 import sys
-import tarfile
-import tempfile
 
+from packaging.version import Version
 from setuptools import find_packages, setup
 from setuptools.command.build_py import build_py
 from setuptools.command.sdist import sdist
 
 PYTHON_ROOT = os.path.dirname(os.path.abspath(__file__))
-FULL_VERSION_FILE = os.path.join(PYTHON_ROOT, "pypaimon", "_full_version")
-UNKNOWN_COMMIT_ID = "UNKNOWN"
-
-VERSION = "2.2.dev"
-
-
-def _repository_root():
-    parent = os.path.dirname(PYTHON_ROOT)
-    if os.path.basename(PYTHON_ROOT) == "paimon-python" and os.path.exists(
-            os.path.join(parent, "pom.xml")):
-        return parent
-    return PYTHON_ROOT
-
-
-def _git_output(args):
-    repository_root = _repository_root()
-    env = os.environ.copy()
-    env["GIT_CEILING_DIRECTORIES"] = os.path.dirname(repository_root)
-    try:
-        return subprocess.check_output(
-            ["git", "-C", repository_root] + args,
-            stderr=subprocess.DEVNULL,
-            env=env,
-        ).decode("utf-8").strip()
-    except Exception:
-        return None
-
-
-def _full_version():
-    try:
-        with open(FULL_VERSION_FILE, "r") as full_version_file:
-            embedded = full_version_file.read().strip()
-            if embedded:
-                return embedded
-    except OSError:
-        pass
-
-    git_commit_id = _git_output(["rev-parse", "HEAD"])
-    if git_commit_id is None:
-        git_commit_id = UNKNOWN_COMMIT_ID
-    return "python-{}-{}".format(VERSION, git_commit_id)
+BUILD_INFO = runpy.run_path(os.path.join(PYTHON_ROOT, "pypaimon", 
"build_info.py"))
+VERSION = str(Version(os.environ.get("PYPAIMON_BUILD_VERSION") or
+                      BUILD_INFO["package_version"]()))
+COMMIT_ID = BUILD_INFO["full_version"]().rsplit("-", 1)[-1]
+FULL_VERSION = "python-{}-{}".format(VERSION, COMMIT_ID)
 
 
 def _write_full_version(root):
     package_dir = os.path.join(root, "pypaimon")
-    if not os.path.exists(package_dir):
-        os.makedirs(package_dir)
-    with open(os.path.join(package_dir, "_full_version"), "w") as 
full_version_file:
-        full_version_file.write(_full_version() + "\n")
+    os.makedirs(package_dir, exist_ok=True)
+    with open(os.path.join(package_dir, "_full_version"), "w") as version_file:
+        version_file.write(FULL_VERSION + "\n")
 
 
 class PaimonBuildPy(build_py):
@@ -93,254 +54,44 @@ class PaimonSdist(sdist):
         _write_full_version(base_dir)
 
 
-def get_dev_version():
-    """Generate dev version with commit date.
-    Format: 2.2.devYYYYMMDD (e.g. 2.2.dev20260911)
-    Uses the commit date (author date) for reproducibility.
-    """
-    base = VERSION.rstrip(".")
-    if not base.endswith("dev"):
-        return None
-
-    try:
-        date_str = _git_output(
-            ["log", "-1", "--format=%cd", "--date=format:%Y%m%d"])
-        if date_str is None:
-            raise RuntimeError("Git commit date is unavailable")
-    except Exception:
-        print("Warning: git not available, skipping dev package.")
-        return None
-
-    return base + date_str
-
-
-def _build_dev_package():
-    """After sdist completes, repack a copy with dev version."""
-    if "sdist" not in sys.argv:
-        return
-
-    dev_version = get_dev_version()
-    if dev_version is None:
-        return
-
-    from packaging.version import Version
-    normalized = str(Version(VERSION))
-    dev_normalized = str(Version(dev_version))
-
-    src_name = "pypaimon-{}".format(normalized)
-    dev_name = "pypaimon-{}".format(dev_normalized)
-
-    src_tar = os.path.join("dist", src_name + ".tar.gz")
-    if not os.path.exists(src_tar):
-        return
-
-    tmp_dir = tempfile.mkdtemp()
-    try:
-        with tarfile.open(src_tar, "r:gz") as tar:
-            tar.extractall(tmp_dir)
-
-        src_dir = os.path.join(tmp_dir, src_name)
-        dev_dir = os.path.join(tmp_dir, dev_name)
-        os.rename(src_dir, dev_dir)
-
-        # Update version in PKG-INFO files
-        for pkg_info in [
-            os.path.join(dev_dir, "PKG-INFO"),
-            os.path.join(dev_dir, "pypaimon.egg-info", "PKG-INFO"),
-        ]:
-            if os.path.exists(pkg_info):
-                with open(pkg_info, "r") as f:
-                    content = f.read()
-                content = content.replace(
-                    "Version: " + normalized,
-                    "Version: " + dev_normalized
-                )
-                with open(pkg_info, "w") as f:
-                    f.write(content)
-
-        # Update VERSION in setup.py so pip install gets the correct version
-        setup_py = os.path.join(dev_dir, "setup.py")
-        if os.path.exists(setup_py):
-            with open(setup_py, "r") as f:
-                content = f.read()
-            content = content.replace(
-                'VERSION = "' + VERSION + '"',
-                'VERSION = "' + dev_version + '"'
-            )
-            with open(setup_py, "w") as f:
-                f.write(content)
-
-        # Keep the embedded full version consistent with the dev package 
version.
-        full_version_file = os.path.join(dev_dir, "pypaimon", "_full_version")
-        if os.path.exists(full_version_file):
-            with open(full_version_file, "r") as f:
-                content = f.read()
-            version_prefix = "python-{}-".format(VERSION)
-            if content.startswith(version_prefix):
-                content = "python-{}-{}".format(
-                    dev_version, content[len(version_prefix):])
-            with open(full_version_file, "w") as f:
-                f.write(content)
-
-        dev_tar = os.path.join("dist", dev_name + ".tar.gz")
-        with tarfile.open(dev_tar, "w:gz") as tar:
-            tar.add(dev_dir, arcname=dev_name)
-
-        print("Created dev package: " + dev_tar)
-    finally:
-        shutil.rmtree(tmp_dir)
-
-
-atexit.register(_build_dev_package)
-
-PACKAGES = find_packages(
-    include=["pypaimon*"],
-    exclude=["pypaimon.tests*", "pypaimon.acceptance*"],
-)
-
-
-def read_requirements():
-    """Read requirements from dev/requirements.txt file."""
-    requirements_path = os.path.join(os.path.dirname(__file__), 'dev', 
'requirements.txt')
-    requirements = []
-
-    if os.path.exists(requirements_path):
-        with open(requirements_path, 'r', encoding='utf-8') as f:
-            for line in f:
-                line = line.strip()
-                # Skip empty lines and comments
-                if line and not line.startswith('#'):
-                    requirements.append(line)
-
-    return requirements
-
-
-install_requires = read_requirements()
-
-VIDEO_DEPENDENCIES = [
-    'av>=12,<19; python_version>="3.10"',
-    'Pillow; python_version>="3.10"',
-]
-
-LEROBOT_DEPENDENCIES = [
-    # datasets 4.1+ may select PyArrow 21+, while PyPaimon currently
-    # supports PyArrow <20. Pandas 2.2.2+ supports NumPy 2.x selected
-    # by LeRobot's media dependencies.
-    'datasets>=4,<4.1; python_version>="3.10"',
-    'pandas>=2.2.2,<3; python_version>="3.10"',
-    'torch>=2.3; python_version>="3.10"',
-    'lerobot>=0.4.4,<0.5; python_version>="3.10"',
-]
+def _legacy_metadata():
+    # Python 3.6's last setuptools does not support PEP 621. Translate the same
+    # TOML metadata instead of keeping another dependency list or dropping 3.6.
+    import tomli
+
+    with open(os.path.join(PYTHON_ROOT, "pyproject.toml"), "rb") as 
project_file:
+        config = tomli.load(project_file)
+    project = config["project"]
+    setuptools_config = config["tool"]["setuptools"]
+    with open(os.path.join(PYTHON_ROOT, project["readme"]), encoding="utf-8") 
as readme:
+        long_description = readme.read()
+    author = project["authors"][0]
+    return dict(
+        name=project["name"],
+        description=project["description"],
+        long_description=long_description,
+        long_description_content_type="text/markdown",
+        license=project["license"]["text"],
+        author=author["name"],
+        author_email=author["email"],
+        url=project["urls"]["Homepage"],
+        project_urls=project["urls"],
+        python_requires=project["requires-python"],
+        classifiers=project["classifiers"],
+        install_requires=project["dependencies"],
+        extras_require=project["optional-dependencies"],
+        entry_points={"console_scripts": [
+            "{}={}".format(name, entry) for name, entry in 
project["scripts"].items()
+        ]},
+        packages=find_packages(**setuptools_config["packages"]["find"]),
+        include_package_data=setuptools_config["include-package-data"],
+        package_data=setuptools_config["package-data"],
+        license_files=setuptools_config["license-files"],
+    )
 
-long_description = "See Apache Paimon Python API \
-[Doc](https://paimon.apache.org/docs/master/pypaimon/python-api/) for usage."
 
 setup(
-    name="pypaimon",
     version=VERSION,
-    packages=PACKAGES,
-    include_package_data=True,
-    package_data={
-        "pypaimon": [
-            "_full_version",
-            "benchmark/act/default_experiment.json",
-        ],
-    },
     cmdclass={"build_py": PaimonBuildPy, "sdist": PaimonSdist},
-    install_requires=install_requires,
-    entry_points={
-        'console_scripts': [
-            'paimon=pypaimon.cli:main',
-        ],
-    },
-    extras_require={
-        'video': VIDEO_DEPENDENCIES,
-        'hdf5': [
-            # HDF5 loading is explicitly guarded and documented as Python 3.8+.
-            'h5py>=3,<4; python_version>="3.8"',
-        ],
-        'rosbag': [
-            # rosbags is pure Python and does not require a ROS installation.
-            'rosbags>=0.11.5,<0.12; python_version>="3.10"',
-        ],
-        'lerobot': LEROBOT_DEPENDENCIES,
-        'ray': [
-            'ray>=2.10,<3; python_version>="3.8"',
-        ],
-        'torch': [
-            'torch',
-            'Pillow',
-        ],
-        'act': LEROBOT_DEPENDENCIES + [
-            'Pillow; python_version>="3.10"',
-        ],
-        'daft': [
-            'daft>=0.7.6; python_version>="3.10"',
-        ],
-        'oss': [
-            'oss2>=2.18,<3',
-            'ossfs>=2021.8; python_version<"3.8"',
-            'ossfs>=2023; python_version>="3.8"'
-        ],
-        'jindo': [
-            'oss2>=2.18,<3',
-            'pyjindosdk>=6.10.4',
-        ],
-        'lance': [
-            'pylance>=0.20,<1; python_version>="3.9"',
-            'pylance>=0.10,<1; python_version>="3.8" and python_version<"3.9"'
-        ],
-        'vortex': [
-            'vortex-data==0.70.0; python_version>="3.11"',
-        ],
-        'mosaic': [
-            'paimon-mosaic>=0.1.0; python_version>="3.9"',
-        ],
-        'lumina': [
-            'lumina-data>=0.1.0'
-        ],
-        'vindex': [
-            'paimon-vindex==0.5.0; python_version>="3.9"',
-        ],
-        'full-text': [
-            'paimon-ftindex==0.1.0; python_version>="3.8"',
-        ],
-        'theta-sketch': [
-            'datasketches>=4,<5; python_version<"3.9"',
-            'datasketches>=5,<6; python_version>="3.9"',
-        ],
-        'hll-sketch': [
-            'datasketches>=4,<5; python_version<"3.9"',
-            'datasketches>=5,<6; python_version>="3.9"',
-        ],
-        'datafusion': [
-            'datafusion>=54,<55; python_version>="3.10"',
-        ],
-        'sql': [
-            'pypaimon-rust>=0.3.0; python_version>="3.10"',
-            'datafusion>=54,<55; python_version>="3.10"',
-        ],
-        'hdfs': [
-            'hdfs-native>=0.13,<1; python_version >= "3.10" and 
platform_system != "Windows"',
-        ],
-    },
-    description="Apache Paimon Python API",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    author="Apache Software Foundation",
-    author_email="[email protected]",
-    url="https://paimon.apache.org";,
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "License :: OSI Approved :: Apache Software License",
-        "Programming Language :: Python :: 3.6",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Programming Language :: Python :: 3.13",
-    ],
-    python_requires=">=3.6",
+    **(_legacy_metadata() if sys.version_info < (3, 7) else {})
 )

Reply via email to