ソースを参照

Build and release processes improvements (#2499)

- Package dependencies fixed:
    M.m.t depends on >=M.m,<M.(m+1)
- 'taipy' now is a first-class citizen for single-package build
- Favor pyproject vs setup (simplified MANIFEST.in).
  - Enforce Python <3.13 in all pyprojet.toml
- Workflows UI simplified.
- Upgrade codeql-action version.
- Release-dedicated scripts:
  - Added comments and command line argument checks.
  - Added tests on release-dedicated scripts
  - Added Package and Version classes for more code reuse.
  - Fixed missing versions fetched from Pypi.
- Optimized taipy package build.
- Added version check on frontend bundle versions.
- GH workflows can run from any remote
- Allow to force a dependency version in the dependent's setup.requirements.txt file.
Fabien Lelaquais 1 ヶ月 前
コミット
7b9c5f6688
49 ファイル変更1300 行追加798 行削除
  1. 94 71
      .github/workflows/build-and-release-single-package.yml
  2. 132 87
      .github/workflows/build-and-release.yml
  3. 3 3
      .github/workflows/codeql-analysis.yml
  4. 10 0
      .github/workflows/publish.yml
  5. 1 0
      .gitignore
  6. 1 1
      frontend/taipy-gui/package.json
  7. 2 2
      frontend/taipy-gui/src/components/Taipy/TableSort.tsx
  8. 1 1
      frontend/taipy/package.json
  9. 7 3
      pyproject.toml
  10. 9 19
      taipy/common/pyproject.toml
  11. 1 1
      taipy/common/setup.py
  12. 1 1
      taipy/common/version.json
  13. 11 10
      taipy/core/pyproject.toml
  14. 1 1
      taipy/core/setup.py
  15. 1 1
      taipy/core/version.json
  16. 9 8
      taipy/gui/pyproject.toml
  17. 1 1
      taipy/gui/setup.py
  18. 1 1
      taipy/gui/version.json
  19. 9 6
      taipy/rest/pyproject.toml
  20. 1 1
      taipy/rest/version.json
  21. 6 6
      taipy/templates/pyproject.toml
  22. 1 1
      taipy/templates/setup.py
  23. 1 1
      taipy/templates/version.json
  24. 1 1
      taipy/version.json
  25. 0 16
      tests/tools/release/__init__.py
  26. 156 0
      tests/tools/release/test_version.py
  27. 23 3
      tools/frontend/bundle_build.py
  28. 3 2
      tools/packages/taipy-common/MANIFEST.in
  29. 4 30
      tools/packages/taipy-common/setup.py
  30. 3 2
      tools/packages/taipy-core/MANIFEST.in
  31. 4 26
      tools/packages/taipy-core/setup.py
  32. 3 2
      tools/packages/taipy-gui/MANIFEST.in
  33. 4 54
      tools/packages/taipy-gui/setup.py
  34. 3 2
      tools/packages/taipy-rest/MANIFEST.in
  35. 4 15
      tools/packages/taipy-rest/setup.py
  36. 3 2
      tools/packages/taipy-templates/MANIFEST.in
  37. 6 18
      tools/packages/taipy-templates/setup.py
  38. 0 22
      tools/packages/taipy/MANIFEST.in
  39. 3 36
      tools/packages/taipy/setup.py
  40. 134 9
      tools/release/build_package_structure.py
  41. 56 0
      tools/release/check_package_version.py
  42. 20 10
      tools/release/check_releases.py
  43. 294 0
      tools/release/common.py
  44. 123 66
      tools/release/fetch_latest_versions.py
  45. 0 97
      tools/release/setup_project.py
  46. 67 90
      tools/release/setup_version.py
  47. 0 43
      tools/release/update_setup.py
  48. 68 25
      tools/release/update_setup_requirements.py
  49. 14 1
      tools/validate_taipy_install.py

+ 94 - 71
.github/workflows/build-and-release-single-package.yml

@@ -1,22 +1,38 @@
-name: Build and release one taipy sub-package
+name: Build and release one taipy package
 
 on:
   workflow_dispatch:
     inputs:
-      internal_dep_on_pypi:
-        description: "Point taipy internal dependencies to Pypi? If false it will point to the github .tar.gz release file"
-        default: "false"
-        required: true
-      release_type:
-        description: "The type of release to be made (dev or production)"
-        default: "dev"
+      target_package:
+        description: "Package name"
         required: true
+        type: choice
+        options:
+          - gui
+          - common
+          - core
+          - rest
+          - templates
+          - taipy
       target_version:
-        description: "The version of the package to be released"
+        description: "Package version"
         required: true
-      target_package:
-        description: "The package to be released (gui, common, core, rest, templates, taipy)"
+      release_type:
+        description: "Release type"
+        required: true
+        type: choice
+        options:
+          - dev
+          - production
+        default: "dev"
+      sub_packages_location:
+        description: "Dependencies location"
         required: true
+        type: choice
+        options:
+          - GitHub
+          - Pypi
+        default: "GitHub"
 
 env:
   NODE_OPTIONS: --max-old-space-size=4096
@@ -34,30 +50,53 @@ jobs:
         rest_VERSION: ${{ steps.version-setup.outputs.rest_VERSION }}
         templates_VERSION: ${{ steps.version-setup.outputs.templates_VERSION }}
         taipy_VERSION: ${{ steps.version-setup.outputs.taipy_VERSION }}
+        LATEST_TAIPY_VERSION: ${{ steps.version-setup.outputs.LATEST_TAIPY_VERSION }}
     steps:
       - uses: actions/checkout@v4
       - name: Extract branch name
+        id: extract_branch
         shell: bash
         run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
-        id: extract_branch
+
+      - name: Install mandatory Python packages
+        run: |
+          python -m pip install --upgrade pip
+          pip install requests
+
+      - name: Check package version
+        run: |
+          python tools/release/check_package_version.py \
+          ${{ github.event.inputs.target_package }} \
+          ${{ github.event.inputs.target_version }}
 
       - name: Setup Version
         id: version-setup
         run: |
           python tools/release/fetch_latest_versions.py \
-          ${{ github.event.inputs.release_type }} \
-          ${{ github.event.inputs.internal_dep_on_pypi }} \
+          ${{ github.event.inputs.target_package }} \
           ${{ github.event.inputs.target_version }} \
-          ${{ github.event.inputs.target_package }} >> $GITHUB_OUTPUT
+          ${{ github.event.inputs.release_type }} \
+          ${{ github.event.inputs.sub_packages_location }}  \
+          ${{ github.repository }} >>$GITHUB_OUTPUT
+
+      - name: Verify versions
+        run: |
+          echo 'common' version: ${{ steps.version-setup.outputs.common_VERSION }}
+          echo 'core' version: ${{ steps.version-setup.outputs.core_VERSION }}
+          echo 'gui' version: ${{ steps.version-setup.outputs.gui_VERSION }}
+          echo 'rest' version: ${{ steps.version-setup.outputs.rest_VERSION }}
+          echo 'templates' version: ${{ steps.version-setup.outputs.templates_VERSION }}
+          echo 'taipy' version: ${{ steps.version-setup.outputs.taipy_VERSION }}
+          echo Latest 'taipy' version: ${{ steps.version-setup.outputs.LATEST_TAIPY_VERSION }}
 
   build-and-release-package:
-    needs: [fetch-versions]
+    needs: fetch-versions
     timeout-minutes: 20
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
         with:
-          ssh-key: ${{secrets.DEPLOY_KEY}}
+          ssh-key: ${{ secrets.DEPLOY_KEY }}
       - uses: actions/setup-python@v5
         with:
           python-version: 3.9
@@ -66,54 +105,38 @@ jobs:
           node-version: '20'
 
       - name: Extract commit hash
+        id: extract_hash
         shell: bash
         run: echo "HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-        id: extract_hash
 
-      - name: Set Build Variables
+      - name: Install mandatory Python packages
+        run: |
+          python -m pip install --upgrade pip
+          pip install requests
+
+      - name: Set build variables for ${{ github.event.inputs.target_package }} ${{ github.event.inputs.target_version }}
         id: set-variables
+        shell: bash
         run: |
-          if [ "${{ github.event.inputs.target_package }}" == "common" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.common_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/common" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.common_VERSION}}-common" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-common-${{needs.fetch-versions.outputs.common_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
-          elif [ "${{ github.event.inputs.target_package }}" == "core" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.core_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/core" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.core_VERSION}}-core" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-core-${{needs.fetch-versions.outputs.core_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
-          elif [ "${{ github.event.inputs.target_package }}" == "gui" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.gui_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/gui" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.gui_VERSION}}-gui" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-gui-${{needs.fetch-versions.outputs.gui_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
-          elif [ "${{ github.event.inputs.target_package }}" == "rest" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.rest_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/rest" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.rest_VERSION}}-rest" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-rest-${{needs.fetch-versions.outputs.rest_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
-          elif [ "${{ github.event.inputs.target_package }}" == "templates" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.templates_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/templates" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.templates_VERSION}}-templates" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-templates-${{needs.fetch-versions.outputs.templates_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
+          echo "package_version=${{ github.event.inputs.target_version }}" >> $GITHUB_OUTPUT
+          if [ "${{ github.event.inputs.target_package }}" == "taipy" ]; then
+            echo "release_name=${{ github.event.inputs.target_version }}" >> $GITHUB_OUTPUT
+            echo "tar_path=./dist/${{ github.event.repository.name }}-${{ github.event.inputs.target_version }}.tar.gz" >> $GITHUB_OUTPUT
+          else
+            echo "release_name=${{ github.event.inputs.target_version }}-${{ github.event.inputs.target_package }}" >> $GITHUB_OUTPUT
+            echo "tar_path=./dist/${{ github.event.repository.name }}-${{ github.event.inputs.target_package }}-${{ github.event.inputs.target_version }}.tar.gz" >> $GITHUB_OUTPUT
           fi
-        shell: bash
 
       - name: Update setup.requirements.txt
         run: |
-          python tools/release/update_setup_requirements.py taipy-${{ github.event.inputs.target_package }} \
-            ${{needs.fetch-versions.outputs.common_VERSION}} \
-            ${{needs.fetch-versions.outputs.core_VERSION}} \
-            ${{needs.fetch-versions.outputs.gui_VERSION}} \
-            ${{needs.fetch-versions.outputs.rest_VERSION}} \
-            ${{needs.fetch-versions.outputs.templates_VERSION}} \
-            ${{ github.event.inputs.internal_dep_on_pypi }}
-
-      - name: Copy tools
-        run: |
-          cp -r tools ${{ steps.set-variables.outputs.package_dir }}
+          python tools/release/update_setup_requirements.py ${{ github.event.inputs.target_package }} \
+            ${{ needs.fetch-versions.outputs.common_VERSION }} \
+            ${{ needs.fetch-versions.outputs.core_VERSION }} \
+            ${{ needs.fetch-versions.outputs.gui_VERSION }} \
+            ${{ needs.fetch-versions.outputs.rest_VERSION }} \
+            ${{ needs.fetch-versions.outputs.templates_VERSION }} \
+            ${{ github.event.inputs.sub_packages_location }} \
+            ${{ github.repository }}
 
       - name: Install dependencies
         run: |
@@ -121,7 +144,7 @@ jobs:
           pip install build wheel pipenv mypy black isort
 
       - name: Install GUI dependencies
-        if: github.event.inputs.target_package == 'gui'
+        if: ${{ github.event.inputs.target_package == 'gui' || github.event.inputs.target_package == 'taipy' }}
         run: |
           pipenv install --dev
 
@@ -130,41 +153,41 @@ jobs:
         run: |
           pipenv run python tools/gui/generate_pyi.py
 
-      - name: Build frontends
-        if: github.event.inputs.target_package == 'gui'
+      - name: Build Taipy GUI front-end
+        if: ${{ github.event.inputs.target_package == 'gui' || github.event.inputs.target_package == 'taipy' }}
         run: |
-          python tools/frontend/bundle_build.py
+          python tools/frontend/bundle_build.py gui
 
-      - name: Copy files from tools
+      - name: Build Taipy front-end
+        if: ${{ github.event.inputs.target_package == 'taipy' }}
         run: |
-          cp -r tools/packages/taipy-${{ github.event.inputs.target_package }}/. ${{ steps.set-variables.outputs.package_dir }}
+          python tools/frontend/bundle_build.py taipy
 
       - name: Build Package Structure
-        working-directory: ${{ steps.set-variables.outputs.package_dir }}
         run: |
           python tools/release/build_package_structure.py ${{ github.event.inputs.target_package }}
 
       - name: Build package
-        working-directory: ${{ steps.set-variables.outputs.package_dir }}
+        working-directory: "build_${{ github.event.inputs.target_package }}"
         run: |
           python -m build
           for file in ./dist/*; do mv "$file" "${file//_/-}"; done
 
       - name: Create tag and release
-        working-directory: ${{ steps.set-variables.outputs.package_dir }}
+        working-directory: "build_${{ github.event.inputs.target_package }}"
         run: |
-           if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Release Draft ${{ steps.set-variables.outputs.release_name }}"
-           else
+          if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
+            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Dev Release ${{ steps.set-variables.outputs.release_name }}"
+          else
             gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --title ${{ steps.set-variables.outputs.release_name }} --notes "Release ${{ steps.set-variables.outputs.release_name }}"
-           fi
+          fi
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: Ensure Taipy release is marked as latest
+      - name: Ensure the latest 'taipy' production release is marked as *latest* no matter what
         run: |
-           gh release edit ${{needs.fetch-versions.outputs.taipy_VERSION}} --latest
+          gh release edit ${{ needs.fetch-versions.outputs.LATEST_TAIPY_VERSION }} --latest
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 132 - 87
.github/workflows/build-and-release.yml

@@ -3,17 +3,25 @@ name: Build all taipy packages and release them
 on:
   workflow_dispatch:
     inputs:
-      internal_dep_on_pypi:
-        description: "Point taipy internal dependencies to Pypi? If false it will point to the github .tar.gz release file"
-        default: "false"
+      target_version:
+        description: "Package version"
         required: true
       release_type:
-        description: "The type of release to be made (dev or production)"
-        default: "dev"
+        description: "Release type"
         required: true
-      target_version:
-        description: "The version of the package to be released"
+        type: choice
+        options:
+          - dev
+          - production
+        default: "dev"
+      sub_packages_location:
+        description: "Dependencies location"
         required: true
+        type: choice
+        options:
+          - GitHub
+          - Pypi
+        default: "GitHub"
 
 env:
   NODE_OPTIONS: --max-old-space-size=4096
@@ -26,28 +34,41 @@ jobs:
     runs-on: ubuntu-latest
     outputs:
         common_VERSION: ${{ steps.version-setup.outputs.common_VERSION }}
+        NEXT_common_VERSION: ${{ steps.version-setup.outputs.NEXT_common_VERSION }}
         core_VERSION: ${{ steps.version-setup.outputs.core_VERSION }}
+        NEXT_core_VERSION: ${{ steps.version-setup.outputs.NEXT_core_VERSION }}
         gui_VERSION: ${{ steps.version-setup.outputs.gui_VERSION }}
+        NEXT_gui_VERSION: ${{ steps.version-setup.outputs.NEXT_gui_VERSION }}
         rest_VERSION: ${{ steps.version-setup.outputs.rest_VERSION }}
+        NEXT_rest_VERSION: ${{ steps.version-setup.outputs.NEXT_rest_VERSION }}
         templates_VERSION: ${{ steps.version-setup.outputs.templates_VERSION }}
-        VERSION: ${{ steps.version-setup.outputs.VERSION }}
-        NEW_VERSION: ${{ steps.version-setup.outputs.NEW_VERSION }}
+        NEXT_templates_VERSION: ${{ steps.version-setup.outputs.NEXT_templates_VERSION }}
+        taipy_VERSION: ${{ steps.version-setup.outputs.taipy_VERSION }}
+        NEXT_taipy_VERSION: ${{ steps.version-setup.outputs.NEXT_taipy_VERSION }}
+        LATEST_TAIPY_VERSION: ${{ steps.version-setup.outputs.LATEST_TAIPY_VERSION }}
     steps:
       - uses: actions/checkout@v4
       - name: Extract branch name
+        id: extract_branch
         shell: bash
         run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
-        id: extract_branch
+
+      - name: Install mandatory Python packages
+        run: |
+          python -m pip install --upgrade pip
+          pip install requests
 
       - name: Setup Version
         id: version-setup
         run: |
-          python tools/release/setup_version.py ALL ${{ github.event.inputs.release_type }} ${{ github.event.inputs.target_version }} ${{ steps.extract_branch.outputs.branch }} >> $GITHUB_OUTPUT
+          python tools/release/setup_version.py ALL ${{ github.event.inputs.release_type }} \
+          ${{ github.event.inputs.target_version }} \
+          ${{ steps.extract_branch.outputs.branch }} >> $GITHUB_OUTPUT
 
   build-and-release-taipy-packages:
     needs: [fetch-versions]
-    timeout-minutes: 20
     runs-on: ubuntu-latest
+    timeout-minutes: 20
     strategy:
       matrix:
         package: [common, core, gui, rest, templates]
@@ -55,7 +76,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
         with:
-          ssh-key: ${{secrets.DEPLOY_KEY}}
+          ssh-key: ${{ secrets.DEPLOY_KEY }}
       - uses: actions/setup-python@v5
         with:
           python-version: 3.9
@@ -64,157 +85,162 @@ jobs:
           node-version: '20'
 
       - name: Extract commit hash
+        id: extract_hash
         shell: bash
         run: echo "HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-        id: extract_hash
+
+      - name: Install mandatory Python packages
+        run: |
+          python -m pip install --upgrade pip
+          pip install requests
 
       - name: Set Build Variables
         id: set-variables
+        shell: bash
         run: |
           if [ "${{ matrix.package }}" == "common" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.common_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/common" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.common_VERSION}}-common" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-common-${{needs.fetch-versions.outputs.common_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
+            echo "package_version=${{ needs.fetch-versions.outputs.common_VERSION }}" >> $GITHUB_OUTPUT
+            echo "release_name=${{ needs.fetch-versions.outputs.common_VERSION }}-common" >> $GITHUB_OUTPUT
+            echo "tar_path=./dist/${{ github.event.repository.name }}-common-${{ needs.fetch-versions.outputs.common_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "core" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.core_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/core" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.core_VERSION}}-core" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-core-${{needs.fetch-versions.outputs.core_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
+            echo "package_version=${{ needs.fetch-versions.outputs.core_VERSION }}" >> $GITHUB_OUTPUT
+            echo "release_name=${{ needs.fetch-versions.outputs.core_VERSION }}-core" >> $GITHUB_OUTPUT
+            echo "tar_path=./dist/${{ github.event.repository.name }}-core-${{ needs.fetch-versions.outputs.core_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "gui" ]; then
             echo "package_version=${{needs.fetch-versions.outputs.gui_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/gui" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.gui_VERSION}}-gui" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-gui-${{needs.fetch-versions.outputs.gui_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
+            echo "release_name=${{ needs.fetch-versions.outputs.gui_VERSION }}-gui" >> $GITHUB_OUTPUT
+            echo "tar_path=./dist/${{ github.event.repository.name }}-gui-${{ needs.fetch-versions.outputs.gui_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "rest" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.rest_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/rest" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.rest_VERSION}}-rest" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-rest-${{needs.fetch-versions.outputs.rest_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
+            echo "package_version=${{ needs.fetch-versions.outputs.rest_VERSION }}" >> $GITHUB_OUTPUT
+            echo "release_name=${{ needs.fetch-versions.outputs.rest_VERSION }}-rest" >> $GITHUB_OUTPUT
+            echo "tar_path=./dist/${{ github.event.repository.name }}-rest-${{ needs.fetch-versions.outputs.rest_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
           elif [ "${{ matrix.package }}" == "templates" ]; then
-            echo "package_version=${{needs.fetch-versions.outputs.templates_VERSION}}" >> $GITHUB_OUTPUT
-            echo "package_dir=./taipy/templates" >> $GITHUB_OUTPUT
-            echo "release_name=${{needs.fetch-versions.outputs.templates_VERSION}}-templates" >> $GITHUB_OUTPUT
-            echo "tar_path=./dist/${{ github.event.repository.name }}-templates-${{needs.fetch-versions.outputs.templates_VERSION}}.tar.gz" >> $GITHUB_OUTPUT
+            echo "package_version=${{ needs.fetch-versions.outputs.templates_VERSION }}" >> $GITHUB_OUTPUT
+            echo "release_name=${{ needs.fetch-versions.outputs.templates_VERSION }}-templates" >> $GITHUB_OUTPUT
+            echo "tar_path=./dist/${{ github.event.repository.name }}-templates-${{ needs.fetch-versions.outputs.templates_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
           fi
-        shell: bash
 
       - name: Update setup.requirements.txt
         run: |
-          python tools/release/update_setup_requirements.py taipy-${{ matrix.package }} \
-            ${{needs.fetch-versions.outputs.common_VERSION}} \
-            ${{needs.fetch-versions.outputs.core_VERSION}} \
-            ${{needs.fetch-versions.outputs.gui_VERSION}} \
-            ${{needs.fetch-versions.outputs.rest_VERSION}} \
-            ${{needs.fetch-versions.outputs.templates_VERSION}} \
-            ${{ github.event.inputs.internal_dep_on_pypi }}
-
-      - name: Copy tools
-        run: |
-          cp -r tools ${{ steps.set-variables.outputs.package_dir }}
+          python tools/release/update_setup_requirements.py ${{ matrix.package }} \
+            ${{ needs.fetch-versions.outputs.common_VERSION }} \
+            ${{ needs.fetch-versions.outputs.core_VERSION }} \
+            ${{ needs.fetch-versions.outputs.gui_VERSION }} \
+            ${{ needs.fetch-versions.outputs.rest_VERSION }} \
+            ${{ needs.fetch-versions.outputs.templates_VERSION }} \
+            ${{ github.event.inputs.sub_packages_location }} \
+            ${{ github.repository }}
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
           pip install build wheel pipenv mypy black isort
 
-      - name: Install GUI dependencies
+      - name: Build GUI front-end
         if: matrix.package == 'gui'
         run: |
           pipenv install --dev
-
-      - name: Generate GUI pyi file
-        if: matrix.package == 'gui'
-        run: |
           pipenv run python tools/gui/generate_pyi.py
+          python tools/frontend/bundle_build.py gui
 
-      - name: Build frontends
+      - name: Archive the GUI front-end
         if: matrix.package == 'gui'
         run: |
-          python tools/frontend/bundle_build.py
+          tar -czf gui-frontend.tar.gz taipy/gui/webapp
 
-      - name: Copy files from tools
-        run: |
-          cp -r tools/packages/taipy-${{matrix.package}}/. ${{ steps.set-variables.outputs.package_dir }}
+      - name: Upload front-end archive as an artifact
+        if: matrix.package == 'gui'
+        uses: actions/upload-artifact@v4
+        with:
+          name: gui-frontend
+          path: gui-frontend.tar.gz
 
       - name: Build Package Structure
-        working-directory: ${{ steps.set-variables.outputs.package_dir }}
         run: |
           python tools/release/build_package_structure.py ${{ matrix.package }}
 
       - name: Build package
-        working-directory: ${{ steps.set-variables.outputs.package_dir }}
+        working-directory: "build_${{ matrix.package }}"
         run: |
           python -m build
           for file in ./dist/*; do mv "$file" "${file//_/-}"; done
 
-      - name: Create tag and release
-        working-directory: ${{ steps.set-variables.outputs.package_dir }}
+      - name: Create tag and release ${{ steps.set-variables.outputs.release_name }}
+        working-directory: "build_${{ matrix.package }}"
         run: |
-           if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Release Draft ${{ steps.set-variables.outputs.release_name }}"
-           else
+          if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
+            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Dev Release ${{ steps.set-variables.outputs.release_name }}"
+          else
             gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --title ${{ steps.set-variables.outputs.release_name }} --notes "Release ${{ steps.set-variables.outputs.release_name }}"
-           fi
+          fi
         shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
   build-and-release-taipy:
-    runs-on: ubuntu-latest
     needs: [build-and-release-taipy-packages, fetch-versions]
+    runs-on: ubuntu-latest
     timeout-minutes: 20
     steps:
       - uses: actions/checkout@v4
         with:
-          ssh-key: ${{secrets.DEPLOY_KEY}}
+          ssh-key: ${{ secrets.DEPLOY_KEY }}
+
       - name: Extract commit hash
+        id: extract_hash
         shell: bash
         run: echo "HASH=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-        id: extract_hash
 
       - name: Set Build Variables
         id: set-variables
+        shell: bash
         run: |
-          echo "package_version=${{needs.fetch-versions.outputs.VERSION}}" >> $GITHUB_OUTPUT
-          echo "release_name=${{needs.fetch-versions.outputs.VERSION}}" >> $GITHUB_OUTPUT
-          echo "tar_path=./dist/${{ github.event.repository.name }}-${{needs.fetch-versions.outputs.VERSION}}.tar.gz" >> $GITHUB_OUTPUT
+          echo "package_version=${{ needs.fetch-versions.outputs.taipy_VERSION }}" >> $GITHUB_OUTPUT
+          echo "release_name=${{ needs.fetch-versions.outputs.taipy_VERSION }}" >> $GITHUB_OUTPUT
+          echo "tar_path=./dist/${{ github.event.repository.name }}-${{ needs.fetch-versions.outputs.taipy_VERSION }}.tar.gz" >> $GITHUB_OUTPUT
 
       - name: Update setup.requirements.txt
         run: |
           python tools/release/update_setup_requirements.py taipy \
-            ${{needs.fetch-versions.outputs.common_VERSION}} \
-            ${{needs.fetch-versions.outputs.core_VERSION}} \
-            ${{needs.fetch-versions.outputs.gui_VERSION}} \
-            ${{needs.fetch-versions.outputs.rest_VERSION}} \
-            ${{needs.fetch-versions.outputs.templates_VERSION}} \
-            ${{ github.event.inputs.internal_dep_on_pypi }}
+            ${{ needs.fetch-versions.outputs.common_VERSION }} \
+            ${{ needs.fetch-versions.outputs.core_VERSION }} \
+            ${{ needs.fetch-versions.outputs.gui_VERSION }} \
+            ${{ needs.fetch-versions.outputs.rest_VERSION }} \
+            ${{ needs.fetch-versions.outputs.templates_VERSION }} \
+            ${{ github.event.inputs.sub_packages_location }} \
+            ${{ github.repository }}
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
           pip install build wheel
 
-      - name: Backup setup.py
-        run: |
-          mv setup.py setup.old.py
+      - uses: actions/download-artifact@v4
+        with:
+          name: gui-frontend
+          path: .
+
+      - name: Retrieve the GUI front-end
+        run: tar -xzf gui-frontend.tar.gz
 
-      - name: Copy files from tools
+      - name: Build taipy front-end
         run: |
-          cp -r tools/packages/taipy/. .
+          python tools/frontend/bundle_build.py taipy
 
-      - name: Build Frontend
+      - name: Build taipy Package Structure
         run: |
-          python tools/frontend/bundle_build.py
+          python tools/release/build_package_structure.py taipy
 
-      - name: Build Taipy package
+      - name: Build taipy package
+        working-directory: "build_taipy"
         run: |
           python -m build
 
       - name: Create tag and release Taipy
+        working-directory: "build_taipy"
         run: |
           if [ "${{ github.event.inputs.release_type }}" == "dev" ]; then
-            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Release Draft ${{ steps.set-variables.outputs.release_name }}"
+            gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --prerelease --title ${{ steps.set-variables.outputs.release_name }} --notes "Dev Release ${{ steps.set-variables.outputs.release_name }}"
           else
             gh release create ${{ steps.set-variables.outputs.release_name }} ${{ steps.set-variables.outputs.tar_path }} --target ${{ steps.extract_hash.outputs.HASH }} --title ${{ steps.set-variables.outputs.release_name }} --notes "Release ${{ steps.set-variables.outputs.release_name }}"
           fi
@@ -223,6 +249,7 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Download packages
+        working-directory: "build_taipy"
         run: |
           gh release download ${{ needs.fetch-versions.outputs.common_VERSION }}-common --skip-existing --dir dist
           gh release download ${{ needs.fetch-versions.outputs.core_VERSION }}-core --skip-existing --dir dist
@@ -233,15 +260,33 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Bundle all packages in main release tag
+        working-directory: "build_taipy"
         run: |
-          find dist -type f -print0 | xargs -r0 gh release upload ${{ needs.fetch-versions.outputs.VERSION }} --clobber
+          find dist -type f -print0 | xargs -r0 gh release upload ${{ needs.fetch-versions.outputs.taipy_VERSION }} --clobber
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Ensure the latest 'taipy' production release is marked as *latest*
+        if: github.event.inputs.release_type == 'dev'
+        run: |
+          gh release edit ${{ needs.fetch-versions.outputs.LATEST_TAIPY_VERSION }} --latest
+        shell: bash
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - uses: stefanzweifel/git-auto-commit-action@v5
+        if: github.event.inputs.release_type == 'dev'
         with:
-          file_pattern: '*/version.json'
-          commit_message: Update version to ${{ needs.fetch-versions.outputs.NEW_VERSION }}
+          branch: "feature/update-dev-version-${{ github.run_id }}"
+          create_branch: 'true'
+          file_pattern: '**/version.json'
+          commit_message: Update taipy version to ${{ needs.fetch-versions.outputs.NEXT_taipy_VERSION }}
+
+      - name: Create pull request
+        if: github.event.inputs.release_type == 'dev'
+        run: gh pr create -B develop -H "feature/update-dev-version-${{ github.run_id }}" --title 'Update Dev Version' --body 'Created by Github workflow build-and-release'
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Reset changes
         run: |

+ 3 - 3
.github/workflows/codeql-analysis.yml

@@ -27,12 +27,12 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
 
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+      uses: github/codeql-action/autobuild@v3
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3

+ 10 - 0
.github/workflows/publish.yml

@@ -8,6 +8,16 @@ on:
         required: true
 
 jobs:
+  check-version:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Validate version input
+        run: |
+          if [[ ! "${{ github.event.inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.[a-zA-Z]+[0-9]*)?$ ]]; then
+            echo "❌ Invalid version format. Expected format: M.m.t[.ext]"
+            exit 1
+          fi
+
   test-package:
     timeout-minutes: 20
     runs-on: ubuntu-latest

+ 1 - 0
.gitignore

@@ -11,6 +11,7 @@ Pipfile.lock
 .Python
 env/
 build/
+build_*/
 develop-eggs/
 dist/
 downloads/

+ 1 - 1
frontend/taipy-gui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "taipy-gui",
-  "version": "4.0.2",
+  "version": "4.0.3",
   "private": true,
   "dependencies": {
     "@emotion/react": "^11.10.0",

+ 2 - 2
frontend/taipy-gui/src/components/Taipy/TableSort.tsx

@@ -136,7 +136,7 @@ const SortRow = (props: SortRowProps) => {
         <Grid container size={12} alignItems="center">
             <Grid size={6}>
                 <FormControl margin="dense">
-                    <InputLabel>Column</InputLabel>
+                    <InputLabel>{fieldHeader}</InputLabel>
                     <Tooltip title={fieldHeaderTooltip} placement="top">
                         <Select value={colId || ""} onChange={onColSelect} input={<OutlinedInput label={fieldHeader} />}>
                             {cols.map((col) => (
@@ -248,7 +248,7 @@ const TableSort = (props: TableSortProps) => {
                 anchorOrigin={anchorOrigin}
                 open={showSort}
                 onClose={onShowSortClick}
-                className={getSuffixedClassNames(className, "-filter")}
+                className={getSuffixedClassNames(className, "-sort")}
             >
                 <Grid container sx={gridSx} gap={0.5}>
                     {sorts.map((sd, idx) => (

+ 1 - 1
frontend/taipy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "taipy-gui-core",
-  "version": "4.0.2",
+  "version": "4.0.3",
   "private": true,
   "devDependencies": {
     "@types/react": "^18.0.15",

+ 7 - 3
pyproject.toml

@@ -1,13 +1,13 @@
 [build-system]
-requires = ["setuptools>=42", "wheel"]
+requires = ["setuptools>=76", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "taipy"
 description = "A 360° open-source platform from Python pilots to production-ready web apps."
 readme = "package_desc.md"
-requires-python = ">=3.9"
-license = {text = "Apache License 2.0"}
+requires-python = ">=3.9,<3.13"
+license = {text = "Apache-2.0"}
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
 keywords = ["taipy"]
 classifiers = [
@@ -22,6 +22,9 @@ classifiers = [
 ]
 dynamic = ["version", "dependencies"]
 
+[tool.setuptools.packages]
+find = {include = ["taipy", "taipy.*"]}
+
 [project.optional-dependencies]
 ngrok = ["pyngrok>=5.1,<6.0"]
 image = [
@@ -31,6 +34,7 @@ image = [
 rdp = ["rdp>=0.8"]
 arrow = ["pyarrow>=16.0.0,<19.0"]
 mssql = ["pyodbc>=4"]
+test = ["pytest>=6.0"]
 
 [project.scripts]
 taipy = "taipy._entrypoint:_entrypoint"

+ 9 - 19
taipy/common/pyproject.toml

@@ -1,15 +1,15 @@
 [build-system]
-requires = ["setuptools>=42", "wheel"]
+requires = ["setuptools>=76", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "taipy-common"
 description = "A Taipy package dedicated to provide common data structures, types, classes and functions."
 readme = "package_desc.md"
-requires-python = ">=3.9"
-license = {text = "Apache License 2.0"}
+requires-python = ">=3.9,<3.13"
+license = {text = "Apache-2.0"}
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
-keywords = ["taipy-common"]
+keywords = ["taipy", "taipy-common"]
 classifiers = [
     "Intended Audience :: Developers",
     "License :: OSI Approved :: Apache Software License",
@@ -22,21 +22,11 @@ classifiers = [
 ]
 dynamic = ["version", "dependencies"]
 
-[project.optional-dependencies]
-testing = ["pytest>=3.9"]
-
 [tool.setuptools.packages]
-find = {include = [
-    "taipy",
-    "taipy.common",
-    "taipy.common.*",
-    "taipy.common.config",
-    "taipy.common.config.*",
-    "taipy.common.logger",
-    "taipy.common.logger.*",
-    "taipy.common._cli",
-    "taipy.common._cli.*"
-]}
+find = {include = ["taipy", "taipy.common", "taipy.common.*"]}
+
+[project.optional-dependencies]
+test = ["pytest>=6.0"]
 
 [project.urls]
-homepage = "https://github.com/avaiga/taipy"
+Homepage = "https://github.com/Avaiga/taipy"

+ 1 - 1
taipy/common/setup.py

@@ -33,7 +33,7 @@ with open(version_path) as version_file:
 
 requirements = ["toml>=0.10,<0.11", "deepdiff>=6.2,<6.3"]
 
-test_requirements = ["pytest>=3.8"]
+test_requirements = ["pytest>=6.0"]
 
 setup(
     version=version_string,

+ 1 - 1
taipy/common/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 2}
+{"major": 4, "minor": 0, "patch": 3, "ext": "dev0"}

+ 11 - 10
taipy/core/pyproject.toml

@@ -1,15 +1,15 @@
 [build-system]
-requires = ["setuptools>=42", "wheel"]
+requires = ["setuptools>=76", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "taipy-core"
 description = "A Python library to build powerful and customized data-driven back-end applications."
 readme = "package_desc.md"
-requires-python = ">=3.9"
-license = {text = "Apache License 2.0"}
+requires-python = ">=3.9,<3.13"
+license = {text = "Apache-2.0"}
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
-keywords = ["taipy-core"]
+keywords = ["taipy", "taipy-core"]
 classifiers = [
     "Intended Audience :: Developers",
     "License :: OSI Approved :: Apache Software License",
@@ -22,16 +22,17 @@ classifiers = [
 ]
 dynamic = ["version", "dependencies"]
 
+[tool.setuptools.packages]
+find = {include = ["taipy", "taipy.core", "taipy.core.*"]}
+
 [project.optional-dependencies]
+mongo = ["pymongo[srv]>=4.2.0,<5.0"]
 mssql = ["pyodbc>=4,<4.1"]
 mysql = ["pymysql>1,<1.1"]
-postgresql = ["psycopg2>2.9,<2.10"]
 parquet = ["fastparquet==2022.11.0", "pyarrow>=16.0.0,<19.0"]
+postgresql = ["psycopg2>2.9,<2.10"]
 s3 = ["boto3==1.29.1"]
-mongo = ["pymongo[srv]>=4.2.0,<5.0"]
-
-[tool.setuptools.packages]
-find = {include = ["taipy", "taipy.core", "taipy.core.*"]}
+test = ["pytest>=6.0"]
 
 [project.urls]
-homepage = "https://github.com/avaiga/taipy"
+Homepage = "https://github.com/Avaiga/taipy"

+ 1 - 1
taipy/core/setup.py

@@ -37,7 +37,7 @@ def get_requirements():
 
     return [r for r in reqs if r and not r.startswith("taipy")]
 
-test_requirements = ["pytest>=3.8"]
+test_requirements = ["pytest>=6.0"]
 
 extras_require = {
     "mssql": ["pyodbc>=4,<4.1"],

+ 1 - 1
taipy/core/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 2}
+{"major": 4, "minor": 0, "patch": 3, "ext": "dev0"}

+ 9 - 8
taipy/gui/pyproject.toml

@@ -1,15 +1,15 @@
 [build-system]
-requires = ["setuptools>=42", "wheel"]
+requires = ["setuptools>=76", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "taipy-gui"
 description = "Low-code library to create graphical user interfaces on the Web for your Python applications."
 readme = "package_desc.md"
-requires-python = ">=3.9"
-license = {text = "Apache License 2.0"}
+requires-python = ">=3.9,<3.13"
+license = {text = "Apache-2.0"}
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
-keywords = ["taipy-gui"]
+keywords = ["taipy", "gui", "taipy-gui"]
 classifiers = [
     "Intended Audience :: Developers",
     "License :: OSI Approved :: Apache Software License",
@@ -22,6 +22,9 @@ classifiers = [
 ]
 dynamic = ["version", "dependencies"]
 
+[tool.setuptools.packages]
+find = {include = ["taipy", "taipy.gui", "taipy.gui.*"]}
+
 [project.optional-dependencies]
 ngrok = ["pyngrok>=5.1,<6.0"]
 image = [
@@ -29,9 +32,7 @@ image = [
     "python-magic-bin>=0.4.14,<0.5;platform_system=='Windows'",
 ]
 arrow = ["pyarrow>=16.0.0,<19.0"]
-
-[tool.setuptools.packages]
-find = {include = ["taipy", "taipy.gui", "taipy.gui.*"]}
+test = ["pytest>=6.0"]
 
 [project.urls]
-homepage = "https://github.com/avaiga/taipy"
+Homepage = "https://github.com/Avaiga/taipy"

+ 1 - 1
taipy/gui/setup.py

@@ -37,7 +37,7 @@ def get_requirements():
 
     return [r for r in reqs if r and not r.startswith("taipy")]
 
-test_requirements = ["pytest>=3.8"]
+test_requirements = ["pytest>=6.0"]
 
 extras_require = {
     "ngrok": ["pyngrok>=5.1,<6.0"],

+ 1 - 1
taipy/gui/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 3, "ext": "dev1"}
+{"major": 4, "minor": 0, "patch": 3, "ext": "dev0"}

+ 9 - 6
taipy/rest/pyproject.toml

@@ -1,15 +1,15 @@
 [build-system]
-requires = ["setuptools>=42", "wheel"]
+requires = ["setuptools>=76", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "taipy-rest"
 description = "Library to expose taipy-core REST APIs."
 readme = "package_desc.md"
-requires-python = ">=3.9"
-license = {text = "Apache License 2.0"}
+requires-python = ">=3.9,<3.13"
+license = {text = "Apache-2.0"}
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
-keywords = ["taipy-rest"]
+keywords = ["taipy", "rest", "taipy-rest"]
 classifiers = [
     "Intended Audience :: Developers",
     "License :: OSI Approved :: Apache Software License",
@@ -23,7 +23,10 @@ classifiers = [
 dynamic = ["version", "dependencies"]
 
 [tool.setuptools.packages]
-find = {include = ["taipy", "taipy.rest"]}
+find = {include = ["taipy", "taipy.rest", "taipy.rest.*"]}
+
+[project.optional-dependencies]
+test = ["pytest>=6.0"]
 
 [project.urls]
-homepage = "https://github.com/avaiga/taipy"
+Homepage = "https://github.com/Avaiga/taipy"

+ 1 - 1
taipy/rest/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 2}
+{"major": 4, "minor": 0, "patch": 3, "ext": "dev0"}

+ 6 - 6
taipy/templates/pyproject.toml

@@ -1,15 +1,15 @@
 [build-system]
-requires = ["setuptools>=42", "wheel"]
+requires = ["setuptools>=76", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "taipy-templates"
 description = "An open-source package holding Taipy application templates."
 readme = "package_desc.md"
-requires-python = ">=3.9"
-license = {text = "Apache License 2.0"}
+requires-python = ">=3.9,<3.13"
+license = {text = "Apache-2.0"}
 authors = [{name = "Avaiga", email = "dev@taipy.io"}]
-keywords = ["taipy-templates"]
+keywords = ["taipy", "taipy-templates"]
 classifiers = [
     "Intended Audience :: Developers",
     "License :: OSI Approved :: Apache Software License",
@@ -23,7 +23,7 @@ classifiers = [
 dynamic = ["version", "dependencies"]
 
 [tool.setuptools.packages]
-find = {include = ["taipy"]}
+find = {include = ["taipy", "taipy.templates", "taipy.templates.*"]}
 
 [project.urls]
-homepage = "https://github.com/avaiga/taipy"
+Homepage = "https://github.com/Avaiga/taipy"

+ 1 - 1
taipy/templates/setup.py

@@ -23,7 +23,7 @@ with open(version_path) as version_file:
     if vext := version.get("ext"):
         version_string = f"{version_string}.{vext}"
 
-test_requirements = ["pytest>=3.8"]
+test_requirements = ["pytest>=6.0"]
 
 setup(
     packages=find_namespace_packages(where=".") + find_packages(include=["taipy"]),

+ 1 - 1
taipy/templates/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 2}
+{"major": 4, "minor": 0, "patch": 3, "ext": "dev0"}

+ 1 - 1
taipy/version.json

@@ -1 +1 @@
-{"major": 4, "minor": 0, "patch": 3, "ext": "dev1"}
+{"major": 4, "minor": 0, "patch": 3, "ext": "dev0"}

+ 0 - 16
tools/release/extract_from_setup.py → tests/tools/release/__init__.py

@@ -8,19 +8,3 @@
 # 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.
-
-import sys
-
-
-def extract_gui_version(base_path: str) -> None:
-    with open("setup.py") as f:
-        for line in f:
-            if "taipy-gui" in line:
-                start = line.find("taipy-gui")
-                end = line.rstrip().find('",')
-                print(f"VERSION={line[start:end]}")  # noqa: T201
-                break
-
-
-if __name__ == "__main__":
-    extract_gui_version(sys.argv[1])

+ 156 - 0
tests/tools/release/test_version.py

@@ -0,0 +1,156 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed 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.
+import pytest
+
+from tools.release.common import Version
+
+
+def test_from_string():
+    with pytest.raises(ValueError):
+        Version.from_string("invalid")
+    with pytest.raises(ValueError):
+        Version.from_string("1")
+    with pytest.raises(ValueError):
+        Version.from_string("1.x.2")
+
+    version = Version.from_string("1.2")
+    assert version.major == 1
+    assert version.minor == 2
+    assert version.patch == 0
+    assert version.ext is None
+
+    version = Version.from_string("1.2.3")
+    assert version.major == 1
+    assert version.minor == 2
+    assert version.patch == 3
+    assert version.ext is None
+
+    version = Version.from_string("1.2.3.some_ext")
+    assert version.major == 1
+    assert version.minor == 2
+    assert version.patch == 3
+    assert version.ext == "some_ext"
+
+    version = Version.from_string("1.2.3.some_ext.more_ext")
+    assert version.major == 1
+    assert version.minor == 2
+    assert version.patch == 3
+    assert version.ext == "some_ext.more_ext"
+
+
+def test_extension():
+    version = Version.from_string("1.2.3")
+    extension = version._split_ext()
+    assert extension == ("", -1)
+
+    version = Version.from_string("1.2.3.some_ext")
+    extension = version._split_ext()
+    assert extension == ("some_ext", -1)
+
+    version = Version.from_string("1.2.3.some_ext123")
+    extension = version._split_ext()
+    assert extension == ("some_ext", 123)
+
+
+def test_to_string():
+    version = Version(major=1, minor=2)
+    assert str(version) == "1.2.0"
+
+    version = Version(major=1, minor=2, patch=3)
+    assert str(version) == "1.2.3"
+
+    version = Version(major=1, minor=2, patch=3, ext="some_ext")
+    assert str(version) == "1.2.3.some_ext"
+
+
+def test_to_dict():
+    version = Version(major=1, minor=2, patch=3)
+    assert version.to_dict() == {"major": 1, "minor": 2, "patch": 3}
+
+    version = Version(major=1, minor=2, patch=3, ext="some_ext")
+    assert version.to_dict() == {"major": 1, "minor": 2, "patch": 3, "ext": "some_ext"}
+
+
+def test_compatibility():
+    # Different major version number
+    v1 = Version(major=1, minor=2, patch=3)
+    v2 = Version(major=2, minor=2, patch=3)
+    assert not v1.is_compatible(v2), "Major versions differ"
+
+    # Different minor version number
+    v1 = Version(major=1, minor=2, patch=3)
+    v2 = Version(major=1, minor=3, patch=3)
+    assert not v1.is_compatible(v2), "Minor versions differ"
+
+    # All the same
+    v1 = Version(major=1, minor=2, patch=3)
+    v2 = Version(major=1, minor=2, patch=3)
+    assert v1.is_compatible(v2), "Identical versions"
+
+    # Greater patch number
+    v1 = Version(major=1, minor=2, patch=4)
+    v2 = Version(major=1, minor=2, patch=3)
+    assert v1.is_compatible(v2), "Patch number is greater"
+
+    # Smaller patch number
+    v1 = Version(major=1, minor=2, patch=3)
+    v2 = Version(major=1, minor=2, patch=4)
+    assert v1.is_compatible(v2), "Patch number is smaller"
+
+    # Same patch number, extension
+    v1 = Version(major=1, minor=2, patch=3, ext="ext")
+    v2 = Version(major=1, minor=2, patch=3)
+    assert v1.is_compatible(v2), "Same version, with extension"
+
+    # Same patch number, no extension
+    v1 = Version(major=1, minor=2, patch=3)
+    v2 = Version(major=1, minor=2, patch=3, ext="ext")
+    assert not v1.is_compatible(v2), "Same version, no extension is expected"
+
+    # Same patch number, different extension
+    v1 = Version(major=1, minor=2, patch=3, ext="some_ext")
+    v2 = Version(major=1, minor=2, patch=3, ext="another_ext")
+    assert not v1.is_compatible(v2), "Same version, different extensions"
+
+
+def test_order():
+    v1 = Version(major=1, minor=0)
+    v2 = Version(major=2, minor=0)
+    assert v1 < v2, "Version 1.0 is older than 2.0"
+    assert v2 > v1, "Version 2.0 is newer than 1.0"
+
+    v1 = Version(major=1, minor=0)
+    v2 = Version(major=1, minor=1)
+    assert v1 < v2, "Version 1.0 is older than 1.1"
+    assert v2 > v1, "Version 1.1 is newer than 1.0"
+
+    v1 = Version(major=1, minor=2)
+    v2 = Version(major=2, minor=1)
+    assert v1 < v2, "Version 1.2 is older than 2.1"
+    assert v2 > v1, "Version 2.1 is newer than 1.2"
+
+    v1 = Version(major=1, minor=0)
+    v2 = Version(major=1, minor=0, patch=1)
+    assert v1 < v2, "Version 1.0.0 is older than 1.0.1"
+    assert v2 > v1, "Version 1.0.1 is newer than 1.0.0"
+
+    versions = [Version(1, 0), Version(2, 1), Version(3, 4), Version(2, 0)]
+    assert max(versions) == Version(3, 4), "Cannot find max in Version list"
+
+
+def test_bump_ext():
+    version = Version(major=1, minor=2, patch=3)
+    new_version = version.bump_ext_version()
+    assert version == new_version
+
+    version = Version(major=1, minor=2, patch=3, ext="ext0")
+    new_version = version.bump_ext_version()
+    assert new_version.ext == "ext1"

+ 23 - 3
tools/frontend/bundle_build.py

@@ -8,6 +8,17 @@
 # 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.
+# --------------------------------------------------------------------------------------------------
+# Builds the Taipy GUI or Taipy frontend bundle.
+#
+# Invoked from the workflows:
+#          actions\install\action.yml
+#          workflows\build-and-release.yml
+#          workflows\build-and-release-single-package.yml
+#          workflows\packaging.yml
+#          workflows\partial-tests.yml
+#          workflows\prebuild.yml
+# --------------------------------------------------------------------------------------------------
 
 import platform
 import subprocess
@@ -17,6 +28,14 @@ from pathlib import Path
 with_shell = platform.system() == "Windows"
 
 
+def usage() -> None:
+    print(f"Usage: {sys.argv[0]} [<bundle>]")  # noqa: T201
+    print("   Builds the Taipy frontend bundles.")  # noqa: T201
+    print("   If <bundle> is 'gui', only the Taipy GUI bundle is built.")  # noqa: T201
+    print("   If <bundle> is 'taipy', only the Taipy bundle is built (expecting Taipy GUI's to exist).")  # noqa: T201
+    print("   In all other cases, both bundles are built.")  # noqa: T201
+
+
 def build_gui(root_path: Path):
     print(f"Building taipy-gui frontend bundle in {root_path}.")  # noqa: T201
     already_exists = (root_path / "taipy" / "gui" / "webapp" / "index.html").exists()
@@ -48,8 +67,9 @@ if __name__ == "__main__":
     if len(sys.argv) > 1:
         if sys.argv[1] == "gui":
             build_gui(root_path)
+            exit(0)
         elif sys.argv[1] == "taipy":
             build_taipy(root_path)
-    else:
-        build_gui(root_path)
-        build_taipy(root_path)
+            exit(0)
+    build_gui(root_path)
+    build_taipy(root_path)

+ 3 - 2
tools/packages/taipy-common/MANIFEST.in

@@ -1,6 +1,7 @@
+# Package taipy-common
 include taipy/common/*.json
 include taipy/common/config/*.pyi
 include taipy/common/config/*.json
-include *.json
-include taipy/common/setup.requirements.txt
+
+include setup.requirements.txt
 include package_desc.md

+ 4 - 30
tools/packages/taipy-common/setup.py

@@ -14,43 +14,17 @@
 import json
 from pathlib import Path
 
-from setuptools import find_packages, setup
+from setuptools import setup
 
 root_folder = Path(__file__).parent
 
-package_desc = Path(root_folder / "package_desc.md").read_text("UTF-8")
-
-version_path = "taipy/common/version.json"
-
-setup_requirements = Path("taipy/common/setup.requirements.txt")
-
-with open(version_path) as version_file:
+with open(root_folder / "taipy" / "common" / "version.json") as version_file:
     version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
+    version_string = f'{version.get("major")}.{version.get("minor")}.{version.get("patch")}'
     if vext := version.get("ext"):
         version_string = f"{version_string}.{vext}"
 
-requirements = [r for r in (setup_requirements).read_text("UTF-8").splitlines() if r]
-
-test_requirements = ["pytest>=3.8"]
-
 setup(
     version=version_string,
-    install_requires=requirements,
-    packages=find_packages(
-        where=root_folder, include=[
-            "taipy",
-            "taipy.common",
-            "taipy.common.*",
-            "taipy.common.config",
-            "taipy.common.config.*",
-            "taipy.common.logger",
-            "taipy.common.logger.*",
-            "taipy.common._cli",
-            "taipy.common._cli.*"
-        ]
-    ),
-    include_package_data=True,
-    data_files=[('version', [version_path])],
-    tests_require=test_requirements,
+    install_requires=[r for r in (root_folder / "setup.requirements.txt").read_text("UTF-8").splitlines() if r]
 )

+ 3 - 2
tools/packages/taipy-core/MANIFEST.in

@@ -1,5 +1,6 @@
+# Package taipy-core
 include taipy/core/*.json
 include taipy/core/config/*.json
-include *.json
-include taipy/core/setup.requirements.txt
+
+include setup.requirements.txt
 include package_desc.md

+ 4 - 26
tools/packages/taipy-core/setup.py

@@ -14,39 +14,17 @@
 import json
 from pathlib import Path
 
-from setuptools import find_packages, setup
+from setuptools import setup
 
 root_folder = Path(__file__).parent
 
-package_desc = Path(root_folder / "package_desc.md").read_text("UTF-8")
-
-version_path = "taipy/core/version.json"
-
-setup_requirements = Path("taipy/core/setup.requirements.txt")
-
-with open(version_path) as version_file:
+with open(root_folder / "taipy" / "core" / "version.json") as version_file:
     version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
+    version_string = f'{version.get("major")}.{version.get("minor")}.{version.get("patch")}'
     if vext := version.get("ext"):
         version_string = f"{version_string}.{vext}"
 
-requirements = [r for r in (setup_requirements).read_text("UTF-8").splitlines() if r]
-
-test_requirements = ["pytest>=3.8"]
-
-extras_require = {
-    "fastparquet": ["fastparquet==2022.11.0"],
-    "mssql": ["pyodbc>=4,<4.1"],
-    "mysql": ["pymysql>1,<1.1"],
-    "postgresql": ["psycopg2>2.9,<2.10"],
-}
-
 setup(
     version=version_string,
-    install_requires=requirements,
-    packages=find_packages(where=root_folder, include=["taipy", "taipy.core", "taipy.core.*"]),
-    include_package_data=True,
-    data_files=[('version', [version_path])],
-    tests_require=test_requirements,
-    extras_require=extras_require,
+    install_requires=[r for r in (root_folder / "setup.requirements.txt").read_text("UTF-8").splitlines() if r]
 )

+ 3 - 2
tools/packages/taipy-gui/MANIFEST.in

@@ -1,7 +1,8 @@
+# Package taipy-gui
 recursive-include taipy/gui/webapp *
 include taipy/gui/version.json
 include taipy/gui/viselements.json
 include taipy/gui/*.pyi
-include *.json
-include taipy/gui/setup.requirements.txt
+
+include setup.requirements.txt
 include package_desc.md

+ 4 - 54
tools/packages/taipy-gui/setup.py

@@ -12,69 +12,19 @@
 """The setup script for taipy-gui package"""
 
 import json
-import os
-import platform
 from pathlib import Path
-import subprocess
 
-from setuptools import find_packages, setup
-from setuptools.command.build_py import build_py
+from setuptools import setup
 
 root_folder = Path(__file__).parent
 
-package_desc = Path(root_folder / "package_desc.md").read_text("UTF-8")
-
-version_path = "taipy/gui/version.json"
-
-setup_requirements = Path("taipy/gui/setup.requirements.txt")
-
-with open(version_path) as version_file:
+with open(root_folder / "taipy" / "gui" / "version.json") as version_file:
     version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
+    version_string = f'{version.get("major")}.{version.get("minor")}.{version.get("patch")}'
     if vext := version.get("ext"):
         version_string = f"{version_string}.{vext}"
 
-requirements = [r for r in (setup_requirements).read_text("UTF-8").splitlines() if r]
-
-test_requirements = ["pytest>=3.8"]
-
-extras_require = {
-    "ngrok": ["pyngrok>=5.1,<6.0"],
-    "image": [
-        "python-magic>=0.4.24,<0.5;platform_system!='Windows'",
-        "python-magic-bin>=0.4.14,<0.5;platform_system=='Windows'",
-    ],
-    "arrow": ["pyarrow>=16.0.0,<19.0"],
-}
-
-
-class NPMInstall(build_py):
-    def run(self):
-        with_shell = platform.system() == "Windows"
-        print(f"Building taipy-gui frontend bundle in {root_folder}.")
-        already_exists = (root_folder / "taipy" / "gui" / "webapp" / "index.html").exists()
-        if already_exists:
-            print(f'Found taipy-gui frontend bundle in {root_folder  / "taipy" / "gui" / "webapp"}.')
-        else:
-            subprocess.run(
-                ["npm", "ci"], cwd=root_folder / "frontend" / "taipy-gui" / "dom", check=True, shell=with_shell
-            )
-            subprocess.run(
-                ["npm", "ci"], cwd=root_folder / "frontend" / "taipy-gui", check=True, shell=with_shell,
-            )
-            subprocess.run(
-                ["npm", "run", "build"], cwd=root_folder / "frontend" / "taipy-gui", check=True, shell=with_shell
-            )
-        build_py.run(self)
-
-
 setup(
     version=version_string,
-    install_requires=requirements,
-    packages=find_packages(where=root_folder, include=["taipy", "taipy.gui", "taipy.gui.*"]),
-    include_package_data=True,
-    data_files=[("version", [version_path])],
-    tests_require=test_requirements,
-    extras_require=extras_require,
-    cmdclass={"build_py": NPMInstall},
+    install_requires=[r for r in (root_folder / "setup.requirements.txt").read_text("UTF-8").splitlines() if r]
 )

+ 3 - 2
tools/packages/taipy-rest/MANIFEST.in

@@ -1,4 +1,5 @@
+# Package taipy-rest
 include taipy/rest/*.json
-include *.json
-include taipy/rest/setup.requirements.txt
+
+include setup.requirements.txt
 include package_desc.md

+ 4 - 15
tools/packages/taipy-rest/setup.py

@@ -14,28 +14,17 @@
 import json
 from pathlib import Path
 
-from setuptools import find_packages, setup
+from setuptools import setup
 
 root_folder = Path(__file__).parent
 
-package_desc = Path(root_folder / "package_desc.md").read_text("UTF-8")
-
-version_path = "taipy/rest/version.json"
-
-setup_requirements = Path("taipy/rest/setup.requirements.txt")
-
-with open(version_path) as version_file:
+with open(root_folder / "taipy" / "rest" / "version.json") as version_file:
     version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
+    version_string = f'{version.get("major")}.{version.get("minor")}.{version.get("patch")}'
     if vext := version.get("ext"):
         version_string = f"{version_string}.{vext}"
 
-requirements = [r for r in (setup_requirements).read_text("UTF-8").splitlines() if r]
-
 setup(
     version=version_string,
-    packages=find_packages(where=root_folder, include=["taipy", "taipy.rest", "taipy.rest.*"]),
-    include_package_data=True,
-    data_files=[('version', [version_path])],
-    install_requires=requirements,
+    install_requires=[r for r in (root_folder / "setup.requirements.txt").read_text("UTF-8").splitlines() if r]
 )

+ 3 - 2
tools/packages/taipy-templates/MANIFEST.in

@@ -1,4 +1,5 @@
+# Package taipy-templates
 recursive-include taipy/templates *
-include *.json
-include taipy/templates/setup.requirements.txt
+
+include setup.requirements.txt
 include package_desc.md

+ 6 - 18
tools/packages/taipy-templates/setup.py

@@ -9,35 +9,23 @@
 # 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.
 
-"""The setup script."""
+"""The setup script for taipy-templates package"""
 
 import json
 from pathlib import Path
 
-from setuptools import find_packages, setup
+from setuptools import setup
 
 root_folder = Path(__file__).parent
 
-package_desc = Path(root_folder / "package_desc.md").read_text("UTF-8")
-
-version_path = "taipy/templates/version.json"
-
-setup_requirements = Path("taipy/templates/setup.requirements.txt")
-
-with open(version_path) as version_file:
+with open(root_folder / "taipy" / "templates" / "version.json") as version_file:
     version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
+    version_string = f'{version.get("major")}.{version.get("minor")}.{version.get("patch")}'
     if vext := version.get("ext"):
         version_string = f"{version_string}.{vext}"
 
-requirements = [r for r in (setup_requirements).read_text("UTF-8").splitlines() if r]
-
-test_requirements = ["pytest>=3.8"]
-
 setup(
-    packages=find_packages(where=root_folder, include=["taipy"]),
-    include_package_data=True,
-    data_files=[('version', [version_path])],
-    test_suite="tests",
     version=version_string,
+    install_requires=[r for r in (root_folder / "setup.requirements.txt").read_text("UTF-8").splitlines() if r],
+    test_suite="tests",
 )

+ 0 - 22
tools/packages/taipy/MANIFEST.in

@@ -1,29 +1,7 @@
-recursive-include tools *
-
 # Package taipy
 include taipy/*.json
 include taipy/gui_core/*.json
 include taipy/gui_core/lib/*.js
 
-# Package taipy-config
-include taipy/config/*.pyi
-include taipy/config/*.json
-
-# Package taipy-core
-include taipy/core/*.json
-include taipy/core/config/*.json
-
-# Package taipy-gui
-recursive-include taipy/gui/webapp *
-include taipy/gui/version.json
-include taipy/gui/viselements.json
-include taipy/gui/*.pyi
-
-# Package taipy-rest
-include taipy/rest/*.json
-
-# Package taipy-templates
-recursive-include taipy/templates *
-
 include setup.requirements.txt
 include package_desc.md

+ 3 - 36
tools/packages/taipy/setup.py

@@ -12,52 +12,19 @@
 """The setup script for taipy package"""
 
 import json
-import platform
 from pathlib import Path
-import subprocess
 
-from setuptools import find_packages, setup
-from setuptools.command.build_py import build_py
+from setuptools import setup
 
 root_folder = Path(__file__).parent
 
-package_desc = (root_folder / "package_desc.md").read_text("UTF-8")
-
 with open(root_folder / "taipy" / "version.json") as version_file:
     version = json.load(version_file)
-    version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
+    version_string = f'{version.get("major")}.{version.get("minor")}.{version.get("patch")}'
     if vext := version.get("ext"):
         version_string = f"{version_string}.{vext}"
 
-requirements = [r for r in (root_folder / "setup.requirements.txt").read_text("UTF-8").splitlines() if r]
-
-test_requirements = ["pytest>=3.8"]
-
-
-class NPMInstall(build_py):
-    def run(self):
-        subprocess.run(
-            ["python", "bundle_build.py"],
-            cwd=root_folder / "tools" / "frontend",
-            check=True,
-            shell=platform.system() == "Windows",
-        )
-        build_py.run(self)
-
-
 setup(
     version=version_string,
-    install_requires=requirements,
-    packages=find_packages(include=["taipy", "taipy.*"]),
-    extras_require={
-        "ngrok": ["pyngrok>=5.1,<6.0"],
-        "image": [
-            "python-magic>=0.4.24,<0.5;platform_system!='Windows'",
-            "python-magic-bin>=0.4.14,<0.5;platform_system=='Windows'",
-        ],
-        "rdp": ["rdp>=0.8"],
-        "arrow": ["pyarrow>=16.0.0,<19.0"],
-        "mssql": ["pyodbc>=4"],
-    },
-    cmdclass={"build_py": NPMInstall},
+    install_requires=[r for r in (root_folder / "setup.requirements.txt").read_text("UTF-8").splitlines() if r]
 )

+ 134 - 9
tools/release/build_package_structure.py

@@ -8,24 +8,149 @@
 # 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.
+# --------------------------------------------------------------------------------------------------
+# Builds the structure to hold the package files.
+#
+# Invoked by the workflow files build-and-release-single-package.yml and build-and-release.yml.
+# Working directory must be '[checkout-root]'.
+# --------------------------------------------------------------------------------------------------
 
 import os
+import re
 import shutil
 import sys
 from pathlib import Path
 
-__SKIP = ["LICENSE", "MANIFEST.in", "taipy", "setup.py", "tools", "pyproject.toml"]
+from common import PACKAGES, Package
+
+# Base build directory name
+DEST_ROOT = "build_"
+
+# Files to be copied from taipy/<package> to build directory
+BUILD_CP_FILES = ["README.md", "setup.py"]
+
+# Files to be moved from taipy/<package> to build directory
+BUILD_MV_FILES = ["LICENSE", "package_desc.md", "pyproject.toml"]
+
+# Items to skip while copying directory structure
+SKIP_ITEMS = {
+    "taipy": [
+        "build_taipy",
+        "doc",
+        "frontend",
+        "readme_img",
+        "tests",
+        "tools",
+        ".git",
+        ".github",
+        ".pytest_cache",
+        "node_modules",
+    ],
+    "taipy-gui": [
+        "node_modules",
+    ],
+}
+
+# Regexp identifying subpackage directories in taipy hierarchy
+packages = "|".join(PACKAGES)
+SUB_PACKAGE_DIR_PATTERN = re.compile(rf"taipy/(?:{packages})")
+
+
+# Filters files not to be copied
+def skip_path(path: str, package: Package, parent: str) -> bool:
+    path = path.replace("\\", "/")
+    if path.startswith("./"):
+        path = path[2:]
+    # Specific items per package
+    if (skip_items := SKIP_ITEMS.get(package.short_name, None)) and path in skip_items:
+        return True
+    # Taipy sub-package directories
+    if package.name == "taipy" and SUB_PACKAGE_DIR_PATTERN.fullmatch(path):
+        return True
+    # Others
+    if path.endswith("__pycache__") or path.startswith("build_"):
+        return True
+    return False
+
+
+def usage() -> None:
+    print(f"Usage: {sys.argv[0]} <package>")  # noqa: T201
+    packages = "', '".join(PACKAGES)
+    print(f"   <package> must be one of '{packages}', or 'taipy'.")  # noqa: T201
 
 
 if __name__ == "__main__":
-    _package = sys.argv[1]
-    _package_path = f"taipy/{_package}"
+    if len(sys.argv) < 2:
+        usage()
+        raise ValueError("Missing arguments.")
+
+    package = Package(sys.argv[1])
+
+    if package.name == "taipy":
+        # Check that gui_core bundle was built
+        if not os.path.exists("taipy/gui_core/lib/taipy-gui-core.js"):
+            raise SystemError("Taipy GUI-Core bundle was not built")
+    elif package.name == "gui":
+        # Check that gui bundle was built
+        if not os.path.exists("taipy/gui/webapp/taipy-gui.js"):
+            raise SystemError("Taipy GUI bundle was not built")
+
+    # Create 'build_<package>' target directory and its subdirectory 'taipy' if needed
+    build_dir = Path(DEST_ROOT + package.short_name)
+    if build_dir.exists():
+        print(f"Removing legacy directory '{build_dir}'")  # noqa: T201
+        shutil.rmtree(build_dir)
+    dest_dir = build_dir
+    if package.name != "taipy":
+        dest_dir = build_dir / "taipy"
+    dest_dir.mkdir(parents=True, exist_ok=True)
+
+    def recursive_copy(source, dest, *, parent: str = "", skip_root: bool = False):
+        dest_path = dest if skip_root else os.path.join(dest, os.path.basename(source))
+        if not skip_root:
+            os.makedirs(dest_path, exist_ok=True)
+
+        for item in os.listdir(source):
+            src_item = os.path.join(source, item)
+            dest_item = os.path.join(dest_path, item)
+            if not skip_path(src_item, package, parent):
+                if os.path.isfile(src_item):
+                    shutil.copy2(src_item, dest_item)
+                elif os.path.isdir(src_item):
+                    if (s := src_item.replace("\\", "/")).startswith("./"):
+                        s = s[2:]
+                    recursive_copy(src_item, dest_path, parent=s)
+
+    # Copy the package build files from taipy[/package] to build_<package>/taipy
+    recursive_copy("." if package.name == "taipy" else package.package_dir, dest_dir)
 
-    Path(_package_path).mkdir(parents=True, exist_ok=True)
+    # This is needed for local builds (i.e. not in a Github workflow)
+    if package.name == "taipy":
+        # Needs the frontend build scripts
+        tools_dir = build_dir / "tools" / "frontend"
+        tools_dir.mkdir(parents=True, exist_ok=True)
+        shutil.copy2("tools/frontend/bundle_build.py", tools_dir)
+        # Copy the build files from tools/packages/taipy to build_taipy
+        recursive_copy(Path("tools") / "packages" / "taipy", build_dir, skip_root=True)
+    else:
+        build_package_dir = build_dir / package.package_dir
+        # Copy build files from package to build dir
+        for filename in BUILD_CP_FILES:
+            shutil.copy2(build_package_dir / filename, build_dir)
+        # Move build files from package to build dir
+        for filename in BUILD_MV_FILES:
+            shutil.move(build_package_dir / filename, build_dir)
+        # Copy the build files from tools/packages/taipy-<package> to build_<package>
+        recursive_copy(Path("tools") / "packages" / f"taipy-{package.short_name}", build_dir, skip_root=True)
 
-    for file_name in os.listdir("."):
-        if file_name.lower().endswith(".md") or file_name in __SKIP:
-            continue
-        shutil.move(file_name, _package_path)
+    # Check that versions were set in setup.requirements.txt
+    with open(build_dir / "setup.requirements.txt") as requirements_file:
+        for line in requirements_file:
+            if match := re.fullmatch(r"(taipy\-\w+)(.*)", line.strip()):
+                if not match[2]:  # Version not updated
+                    print(f"setup.requirements.txt not up-to-date in 'tools/packages/{package.short_name}'.")  # noqa: T201
+                    raise SystemError(f"Version for dependency on {match[1]} is missing.")
 
-    shutil.copy("../__init__.py", "./taipy/__init__.py")
+    # Copy topmost __init__
+    if package.name != "taipy":
+        shutil.copy2(Path("taipy") / "__init__.py", dest_dir)

+ 56 - 0
tools/release/check_package_version.py

@@ -0,0 +1,56 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed 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.
+# --------------------------------------------------------------------------------------------------
+# Checks that the version declared in <package_dir>/version.json matches a given version.
+# --------------------------------------------------------------------------------------------------
+
+import json
+import os
+import sys
+
+from common import Package, Version
+
+
+def usage() -> None:
+    print(f"Usage: {sys.argv[0]} <package> <version>")  # noqa: T201
+    print("   Checks that the version defined in the <package>'s version.json file is <version>.")  # noqa: T201
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        usage()
+        raise ValueError("Missing arguments.")
+    package = Package(sys.argv[1])
+    version = Version.from_string(sys.argv[2])
+
+    # Check that build version matches package's version.json
+    with open(os.path.join(package.package_dir, "version.json")) as version_file:
+        package_version = Version(**json.load(version_file))
+        if version != package_version:
+            raise ValueError(
+                f"Version mismatch for package {package}: "
+                + f"building '{version}' but '{package_version}' is defined in 'version.json'."
+            )
+    # Check Taipy GUI and Taipy packages bundle versions
+    package_json_path = None
+    if package.name == "taipy":
+        package_json_path = "frontend/taipy/package.json"
+    elif package.short_name == "gui":
+        package_json_path = "frontend/taipy-gui/package.json"
+    if package_json_path:
+        with open(package_json_path) as package_file:
+            package_config = json.load(package_file)
+            package_version = Version.from_string(package_config["version"])
+            if version != package_version:
+                raise ValueError(
+                    f"Version mismatch for frontend of package {package}: "
+                    + f"building '{version}' but '{package_version}' is defined in '{package_json_path}'."
+                )

+ 20 - 10
tools/release/check_releases.py

@@ -8,24 +8,34 @@
 # 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.
+# --------------------------------------------------------------------------------------------------
+# Returns the latest released versions for every Taipy package that is compatible with the target
+# version (major and minor numbers match).
+# The target package's version is set to the target version.
+#
+# Invoked from the workflow in publish.yml.
+# --------------------------------------------------------------------------------------------------
 
 import os
 import sys
 
+from common import PACKAGES
+
+
+def usage() -> None:
+    print(f"Usage: {sys.argv[0]} <root_path> <version>")  # noqa: T201
+    print("   Checks that all <package>-<version> archives exist in <root_path>.")  # noqa: T201
+
+
 if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        usage()
+        raise ValueError("Missing arguments.")
+
     _path = sys.argv[1]
     _version = sys.argv[2]
 
-    packages = [
-        f"taipy-{_version}.tar.gz",
-        f"taipy-common-{_version}.tar.gz",
-        f"taipy-core-{_version}.tar.gz",
-        f"taipy-rest-{_version}.tar.gz",
-        f"taipy-gui-{_version}.tar.gz",
-        f"taipy-templates-{_version}.tar.gz",
-    ]
-
-    for package in packages:
+    for package in [f"taipy-{_version}.tar.gz"] + [f"taipy-{p}-{_version}.tar.gz" for p in PACKAGES]:
         if not os.path.exists(os.path.join(_path, package)):
             print(f"Package {package} does not exist")  # noqa: T201
             sys.exit(1)

+ 294 - 0
tools/release/common.py

@@ -0,0 +1,294 @@
+# Copyright 2021-2024 Avaiga Private Limited
+#
+# Licensed 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.
+# --------------------------------------------------------------------------------------------------
+# Common artifacts used by the other scripts located in this directory.
+# --------------------------------------------------------------------------------------------------
+import os
+import re
+import subprocess
+import typing as t
+from dataclasses import asdict, dataclass
+
+import requests
+
+# These are the base name of the sub packages taipy-*
+# They also are the names of the directory where their code belongs, under the 'taipy' directory
+# in the root of the Taipy repository.
+PACKAGES = ["common", "core", "gui", "rest", "templates"]
+
+
+# --------------------------------------------------------------------------------------------------
+@dataclass(order=True)
+class Version:
+    """Helps manipulate version numbers."""
+
+    major: int
+    minor: int
+    patch: int = 0
+    ext: t.Optional[str] = None
+
+    # Unknown version constant
+    UNKNOWN: t.ClassVar["Version"]
+
+    @property
+    def name(self) -> str:
+        """Returns a string representation of this Version without the extension part."""
+        return f"{self.major}.{self.minor}.{self.patch}"
+
+    @property
+    def full_name(self) -> str:
+        """Returns a full string representation of this Version."""
+        return f"{self.name}.{self.ext}" if self.ext else self.name
+
+    def __str__(self) -> str:
+        """Returns a string representation of this version."""
+        return self.full_name
+
+    def __repr__(self) -> str:
+        """Returns a full string representation of this version."""
+        ext = f".{self.ext}" if self.ext else ""
+        return f"Version({self.major}.{self.minor}.{self.patch}{ext})"
+
+    @classmethod
+    def from_string(cls, version: str):
+        """Creates a Version from a string.
+
+        Parameters:
+            version: a version name as a string.<br/>
+              The format should be "<major>.<minor>[.<patch>[.<extension>]] where
+
+              - <major> must be a number, indicating the major number of the version
+              - <minor> must be a number, indicating the minor number of the version
+              - <patch> must be a number, indicating the patch level of the version. Optional.
+              - <extension> must be a string. It is common practice that <extension> ends with a
+                number, but it is not required. Optional.
+        Returns:
+            A new Version object with the appropriate values that were parsed.
+        """
+        match = re.fullmatch(r"(\d+)\.(\d+)(?:\.(\d+))?(?:\.([^\s]+))?", version)
+        if match:
+            major = int(match[1])
+            minor = int(match[2])
+            patch = int(match[3]) if match[3] else 0
+            ext = match[4]
+            return cls(major=major, minor=minor, patch=patch, ext=ext)
+        else:
+            raise ValueError(f"String not in expected format: {version}")
+
+    def to_dict(self) -> dict[str, str]:
+        """Returns this Version as a dictionary."""
+        return {k: v for k, v in asdict(self).items() if v is not None}
+
+    def bump_ext_version(self) -> "Version":
+        """Returns a new Version object where the extension part version was incremented.
+
+        If this Version has no extension part, this method returns *self*.
+        """
+        if not self.ext or (m := re.search(r"([0-9]+)$", self.ext)) is None:
+            return self
+
+        ext_ver = int(m[1]) + 1
+        return Version(self.major, self.minor, self.patch, f"{self.ext[: m.start(1)]}{ext_ver}")
+
+    def validate_extension(self, ext="dev"):
+        """Returns True if the extension part of this Version is the one queried."""
+        return self._split_ext()[0] == ext
+
+    def _split_ext(self) -> t.Tuple[str, int]:
+        """Splits extension into the (identifier, index) tuple
+
+        Returns:
+            ("", -1) if there is no extension.
+            (extension, -1) if there is no extension index.
+            (extension, index) if there is an extension index (e.g. "dev3").
+        """
+        if not self.ext or (match := re.fullmatch(r"(.*?)(\d+)?", self.ext)) is None:
+            return ("", -1)  # No extension
+        # Potentially no index
+        return (match[1], int(match[2]) if match[2] else -1)
+
+    def is_compatible(self, version: "Version") -> bool:
+        """Checks if this version is compatible with another.
+
+        Version v1 is defined as being compatible with version v2 if a package built with version v1
+        can safely depend on another package built with version v2.<br/>
+        Here are the conditions set when checking whether v1 is compatible with v2:
+
+        - If v1 and v2 have different major or minor numbers, they are not compatible.
+        - If v1 has no extension, it is compatible only with v2 that have no extension.
+        - If v1 has an extension, it is compatible with any v2 that has the same extension, no
+          matter the extension index.
+
+        I.e.:
+            package-1.[m].[t] is NOT compatible with any sub-package-[M].* where M != 1
+            package-1.2.[t] is NOT compatible with any sub-package-1.[m].* where m != 2
+            package-1.2.[t] is compatible with all sub-package-1.2.*
+            package-1.2.[t].ext[X] is compatible with all sub-package-1.2.*.ext*
+            package-1.2.3 is NOT compatible with any sub-package-1.2.*.*
+            package-1.2.3.extA is NOT compatible with any sub-package-1.2.*.extB if extA != extB,
+               independently of a potential extension index.
+
+        Parameters:
+            version: the version to check compatibility against.
+
+        Returns:
+            True is this Version is compatible with *version* and False if it is not.
+        """
+        if self.major != version.major or self.minor != version.minor:
+            return False
+        if self.patch > version.patch:
+            return True
+
+        # No extensions on either → Compatible
+        if not self.ext and not version.ext:
+            return True
+
+        # self has extension, version doesn't → Compatible
+        if self.ext and not version.ext:
+            return True
+
+        # Version has extension, self doesn't → Not compatible
+        if not self.ext and version.ext:
+            return False
+
+        # Both have extensions → check identifiers. Dissimilar identifiers → Not compatible
+        self_prefix, _ = self._split_ext()
+        other_prefix, _ = version._split_ext()
+        if self_prefix != other_prefix:
+            return False
+
+        # Same identifiers → Compatible
+        return True
+
+
+Version.UNKNOWN = Version(0, 0)
+
+
+# --------------------------------------------------------------------------------------------------
+class Package:
+    """Information on any Taipy package and sub-package."""
+
+    def __init__(self, package: str) -> None:
+        self._name = package
+        if package == "taipy":
+            self._short = package
+        else:
+            if package.startswith("taipy-"):
+                self._short = package[6:]
+            else:
+                self._name = f"taipy-{package}"
+                self._short = package
+            if self._short not in PACKAGES:
+                raise ValueError(f"Invalid package name {package}.")
+
+    @property
+    def name(self) -> str:
+        """The full package name."""
+        return self._name
+
+    @property
+    def short_name(self) -> str:
+        """The short package name."""
+        return self._short
+
+    @property
+    def package_dir(self) -> str:
+        return "taipy" if self._name == "taipy" else os.path.join("taipy", self._short)
+
+    def __str__(self) -> str:
+        """Returns a string representation of this package."""
+        return self.name
+
+    def __repr__(self) -> str:
+        """Returns a full string representation of this package."""
+        return f"Package({self.name})"
+
+    def __eq__(self, other):
+        return isinstance(other, Package) and (self._short, self._short) == (other._short, other._short)
+
+    def __hash__(self):
+        return hash(self._short)
+
+
+# --------------------------------------------------------------------------------------------------
+def retrieve_github_path() -> t.Optional[str]:
+    # Retrieve current Git branch remote URL
+    def run(*args) -> str:
+        return subprocess.run(args, stdout=subprocess.PIPE, text=True, check=True).stdout.strip()
+
+    branch_name = run("git", "branch", "--show-current")
+    remote_name = run("git", "config", f"branch.{branch_name}.remote")
+    url = run("git", "remote", "get-url", remote_name)
+    if match := re.fullmatch(r"git@github.com:(.*)\.git", url):
+        return match[1]
+    if match := re.fullmatch(r"https://github.com/(.*)$", url):
+        return match[1]
+    print("ERROR - Could not retrieve GibHub branch path")  # noqa: T201
+    return None
+
+
+# --------------------------------------------------------------------------------------------------
+def fetch_github_releases(gh_path: t.Optional[str] = None) -> dict[Package, list[Version]]:
+    # Retrieve all available releases (potentially paginating results) for all packages.
+    # Returns a dictionary of package_short_name-Value pairs.
+    # Note for reviewers: using a Package as the dictionary is is cumbersome in the rest of the
+    # code.
+    all_releases: dict[str, list[Version]] = {}
+    if gh_path is None:
+        gh_path = retrieve_github_path()
+        if gh_path is None:
+            raise ValueError("Couldn't figure out GitHub branch path.")
+    url = f"https://api.github.com/repos/{gh_path}/releases"
+    page = 1
+    # Read all release versions and store them in a package_name - list[Version] dictionary
+    while url:
+        response = requests.get(url, params={"per_page": 50, "page": page})
+        response.raise_for_status()  # Raise error for bad responses
+        for release in response.json():
+            tag_name = release["tag_name"]
+            pkg_ver, pkg = tag_name.split("-") if "-" in tag_name else (tag_name, "taipy")
+            # Drop legacy packages (config...)
+            if pkg != "taipy" and pkg not in PACKAGES:
+                continue
+
+            # Exception for legacy version: v1.0.0 -> 1.0.0
+            if pkg_ver == "v1.0.0":
+                pkg_ver = pkg_ver[1:]
+            version = Version.from_string(pkg_ver)
+            if versions := all_releases.get(pkg):
+                versions.append(version)
+            else:
+                all_releases[pkg] = [version]
+
+        # Check for pagination in the `Link` header
+        link_header = response.headers.get("Link", "")
+        if 'rel="next"' in link_header:
+            url = link_header.split(";")[0].strip("<>")  # Extract next page URL
+            page += 1
+        else:
+            url = None  # No more pages
+
+    # Build and return the dictionary using Package instances
+    return {Package(p): v for p, v in all_releases.items()}
+
+
+# --------------------------------------------------------------------------------------------------
+def fetch_latest_github_taipy_releases(
+    all_releases: t.Optional[dict[Package, list[Version]]] = None, gh_path: t.Optional[str] = None
+) -> Version:
+    # Retrieve all available releases if necessary
+    if all_releases is None:
+        all_releases = fetch_github_releases(gh_path)
+    # Find the latest 'taipy' version that has no extension
+    latest_taipy_version = Version.UNKNOWN
+    if versions := all_releases.get(Package("taipy")):
+        latest_taipy_version = max(filter(lambda v: v.ext is None, versions))
+    return latest_taipy_version

+ 123 - 66
tools/release/fetch_latest_versions.py

@@ -8,84 +8,141 @@
 # 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.
+# --------------------------------------------------------------------------------------------------
+# Returns the latest released versions for every Taipy package that is compatible with the target
+# version (major and minor numbers match).
+# The target package's version is set to the target version.
+#
+# Invoked from the workflow in build-and-release-single-package.yml.
+#
+# Outputs a line for each package (including  'taipy"):
+#   <package_short_name>_VERSION=<package_version>
+# If a dev release is requested, a similar line is issued indicating the next dev version number:
+#   NEXT_<package_short_name>_VERSION=<package_version>
+# An additional line is added containing the latest release for 'taipy', no matter what the target
+# version is. This version is extracted so that it has no extension:
+#   LATEST_TAIPY_VERSION=<package_version>
+# --------------------------------------------------------------------------------------------------
 
 import sys
 
-import requests  # type: ignore
+import requests
+from common import PACKAGES, Package, Version, fetch_github_releases, fetch_latest_github_taipy_releases
 
 
-def fetch_latest_releases_from_github(dev=False, target_version="", target_package=""):
+def usage() -> None:
+    print(f"Usage: {sys.argv[0]} <package> <version> <dev_version> <deps> [<gh_path>]")  # noqa: T201
+    print("   <package> must be a Taipy package name.")  # noqa: T201
+    print("   <version> is the target version for *package*. It must of the form: <Maj>.<Min>.<Tech>[.dev*].")  # noqa: T201
+    print("   <release_type> must be one of 'dev' or 'production'.")  # noqa: T201
+    print("   <deps> must be 'Pypi' or 'GitHub', indicating where to find Taipy package dependencies.")  # noqa: T201
+    print("   <gh_path>: The path of GitHub repository (owner/repo), used if <deps> is 'GitHub'.")  # noqa: T201
+
+
+def fetch_latest_github_releases(
+    package: Package, version: Version, dev: bool, all_releases: dict[Package, list[Version]]
+) -> dict[Package, Version]:
+    """Find the latest release version for each package, in the GitHub releases.
+
+    All release versions are retrieved from GitHub, and we keep the ones that have a version that
+    is compatible with *version*.
+    "dev" releases are kept only if *dev* is True.
+
+    Arguments:
+        package: The package that we want to force *version* for.
+        version: The incoming version of package *package*.
+        dev: True if we're targeting a dev release, False for a production release.
+        gh_path: The "OWNER/REPO" string at the beginning of the working repository.
+          If not provided, it is computed at runtime from the current Git branch remote URL.
+
+    Return:
+        A dictionary make of [package, version] pairs where the *package* package's version is set
+        to *version*.
+    """
+    # For each package, pick the latest that *version* is compatible with
+    all_package_names = PACKAGES + ["taipy"]
     releases = {}
-    url = "https://api.github.com/repos/Avaiga/taipy/releases"
-    response = requests.get(url)
-    resp_json = response.json()
-
-    for rel in resp_json:
-        tag = rel["tag_name"]
-
-        if not dev and ".dev" in tag:
-            continue
-        if "common" in tag:
-            releases["common"] = releases.get("common") or tag.split("-")[0]
-        elif "core" in tag:
-            releases["core"] = releases.get("core") or tag.split("-")[0]
-        elif "gui" in tag:
-            releases["gui"] = releases.get("gui") or tag.split("-")[0]
-        elif "rest" in tag:
-            releases["rest"] = releases.get("rest") or tag.split("-")[0]
-        elif "templates" in tag:
-            releases["templates"] = releases.get("templates") or tag.split("-")[0]
-        elif "-" not in tag:
-            releases["taipy"] = releases.get("taipy") or tag
-    releases[target_package] = target_version
-    return releases
-
-
-def fetch_latest_releases_from_pypi(dev=False, target_version="", target_package=""):
-    def retrieve_package_version(package: str, dev: bool) -> str:
-        url = f"https://pypi.org/pypi/{package}/json"
+    for pkg_name in all_package_names:
+        a_package = Package(pkg_name)
+        if versions := all_releases.get(a_package):
+            for a_version in versions:
+                if a_version.ext and (not dev or not a_version.validate_extension("dev")):
+                    continue
+                if version.is_compatible(a_version):
+                    releases[pkg_name] = a_version
+                    break
+
+    # Fill in missing versions
+    releases[package.short_name] = version
+    for p in all_package_names:
+        if p not in releases:
+            releases[p] = Version.UNKNOWN
+
+    return {Package(p): v for p, v in releases.items()}
+
+
+def fetch_latest_pypi_releases(package: Package, version: Version, dev: bool) -> dict[Package, Version]:
+    """Find the latest release version for each package, in the Pypi releases.
+
+    All release versions are retrieved from Pypi, and we keep the ones that have a version that
+    is compatible with *version*.
+    "dev" releases are kept only if *dev* is True.
+
+    Return:
+        A dictionary make of [package, version] pairs where the *package* package's version is set
+        to *version*.
+    """
+
+    def retrieve_package_version(sub_pkg: Package, dev: bool) -> Version:
+        """Returns the latest release version for *sub_pkg* on Pypi that is compatible with *version*."""
+        url = f"https://pypi.org/pypi/{sub_pkg.name}/json"
         response = requests.get(url)
         resp_json = response.json()
+        # All release versions for the <sub_pkg> package
         versions = list(resp_json["releases"].keys())
-        versions.reverse()
-        return next(v for v in versions if dev or ".dev" not in v)
-
-    releases = {
-        pkg: retrieve_package_version(f"taipy-{pkg}", dev) for pkg in ["common", "core", "gui", "rest", "templates"]
-    }
-    releases["taipy"] = retrieve_package_version("taipy", dev)
-    releases[target_package] = target_version
+        if versions:
+            versions.reverse()  # More recent release is last
+            # Find first that <version> would be compatible with
+            for v in versions:
+                check_version = Version.from_string(v)
+                # Drop all version with extension if not dev
+                # Keep 'dev' extensions if dev
+                if check_version.ext and (not dev or not check_version.validate_extension("dev")):
+                    continue
+                if version.is_compatible(check_version):
+                    return check_version
+        return Version.UNKNOWN
+
+    releases = {pkg: retrieve_package_version(pkg, dev) for pkg in [Package(p) for p in PACKAGES]}
+    releases[package] = version
     return releases
 
 
-def usage() -> None:
-    print(f"Usage: {sys.argv[0]} <dev_version> <is_pypi> <target_version> <target_package>")  # noqa: T201
-    print("   <release_type> must be one of 'dev' or 'production'")  # noqa: T201
-    print("   <from_pypi> must be 'true' or 'false', indicating if dependencies should be pulled out from Pypi")  # noqa: T201
-    print("   <target_version> must of the form: <Maj>.<Min>.<Tech>[.dev*]. Target package version")  # noqa: T201
-    print("   <target_package> must be a Taipy package name")  # noqa: T201
-
-
 if __name__ == "__main__":
     if len(sys.argv) < 5:
         usage()
-        raise ValueError("Version does not contain suffix .dev")
-    is_dev_version = sys.argv[1] == "dev"
-    is_pypi = sys.argv[2] == "true"
-    target_version = sys.argv[3]
-    target_package = sys.argv[4]
-    if target_package.startswith("taipy-"):
-        target_package = target_package[6:]
-
-    if is_dev_version and ".dev" not in target_version:
-        raise ValueError("Version does not contain suffix .dev")
-
-    versions = {}
-
-    if not is_pypi:
-        versions = fetch_latest_releases_from_github(is_dev_version, target_version, target_package)
-    else:
-        versions = fetch_latest_releases_from_pypi(is_dev_version, target_version, target_package)
-
-    for name, version in versions.items():
-        print(f"{name}_VERSION={version}")  # noqa: T201
+        raise ValueError("Missing arguments.")
+    package = Package(sys.argv[1])
+    version = Version.from_string(sys.argv[2])
+
+    is_dev_version = sys.argv[3] == "dev"
+    if is_dev_version and (version.ext is None or not version.validate_extension("dev")):
+        raise ValueError("Version extension does not contain 'dev'.")
+
+    pypi_deps = sys.argv[4] == "Pypi" or sys.argv[4] == "true"  # true to keep pre-4.0.3 compatibility
+    gh_path = sys.argv[5] if len(sys.argv) > 5 else None
+
+    # Retrieve all available Github releases for all packages
+    all_releases = fetch_github_releases(gh_path)
+    # Compute the latest versions compatible with *version*
+    versions = (
+        fetch_latest_pypi_releases(package, version, is_dev_version)
+        if pypi_deps
+        else fetch_latest_github_releases(package, version, is_dev_version, all_releases)
+    )
+    # Print them out
+    for p, v in versions.items():
+        print(f"{p.short_name}_VERSION={v}")  # noqa: T201
+
+    # Print out the latest 'taipy' version that has no extension
+    print(f"LATEST_TAIPY_VERSION={fetch_latest_github_taipy_releases(all_releases)}")  # noqa: T201

+ 0 - 97
tools/release/setup_project.py

@@ -1,97 +0,0 @@
-# Copyright 2021-2024 Avaiga Private Limited
-#
-# Licensed 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.
-
-import json
-import os
-import platform
-import re
-import subprocess
-import sys
-from pathlib import Path
-
-import toml  # type: ignore
-
-
-def get_requirements(pkg: str, env: str = "dev") -> list:
-    # get requirements from the different setups in tools/packages (removing taipy packages)
-    reqs = set()
-    pkg_name = pkg if pkg == "taipy" else f"taipy-{pkg}"
-    root_folder = Path(__file__).parent
-    package_path = os.path.join(root_folder.parent, "packages", pkg_name)
-    requirements_file = os.path.join(package_path, "setup.requirements.txt")
-    if os.path.exists(requirements_file):
-        reqs.update(Path(requirements_file).read_text("UTF-8").splitlines())
-    if env == "dev":
-        return [r for r in reqs if r and not r.startswith("taipy")]
-    return list(reqs)
-
-
-def update_pyproject(version_path: str, pyproject_path: str, env: str = "dev"):
-    with open(version_path) as version_file:
-        version = json.load(version_file)
-        version_string = f'{version.get("major", 0)}.{version.get("minor", 0)}.{version.get("patch", 0)}'
-        if vext := version.get("ext"):
-            version_string = f"{version_string}.{vext}"
-
-    pyproject_data = toml.load(pyproject_path)
-    pyproject_data["project"]["version"] = version_string
-    pyproject_data["project"]["urls"]["Release notes"] = f"https://docs.taipy.io/en/release-{version_string}/relnotes/"
-    pyproject_data["project"]["dependencies"] = get_requirements(get_pkg_name(pyproject_path), env)
-
-    with open(pyproject_path, "w", encoding="utf-8") as pyproject_file:
-        toml.dump(pyproject_data, pyproject_file)
-
-
-def _build_webapp(webapp_path: str):
-    already_exists = Path(webapp_path).exists()
-    if not already_exists:
-        os.system("cd ../../frontend/taipy-gui/dom && npm ci")
-        os.system("cd ../../frontend/taipy-gui && npm ci && npm run build")
-
-
-def get_pkg_name(path: str) -> str:
-    # The regex pattern
-    pattern = r"([^/\\]+)[/\\]pyproject\.toml$"
-
-    # Search for the pattern
-    match = re.search(pattern, os.path.abspath(path))
-    if not match:
-        raise ValueError(f"Could not find package name in path: {path}")
-    return match.group(1)
-
-
-if __name__ == "__main__":
-    _pyproject_path = os.path.join(sys.argv[1], "pyproject.toml")
-    try:
-        env = sys.argv[2]
-    except IndexError:
-        env = "dev"
-
-    pkg = get_pkg_name(_pyproject_path)
-    if pkg == "taipy":
-        _version_path = os.path.join(sys.argv[1], "taipy", "version.json")
-        _webapp_path = os.path.join(sys.argv[1], "taipy", "gui", "webapp", "index.html")
-    else:
-        _version_path = os.path.join(sys.argv[1], "version.json")
-        _webapp_path = os.path.join(sys.argv[1], "webapp", "index.html")
-
-    update_pyproject(_version_path, _pyproject_path, env)
-
-    if pkg == "gui":
-        _build_webapp(_webapp_path)
-
-    if pkg == "taipy":
-        subprocess.run(
-            ["python", "bundle_build.py"],
-            cwd=os.path.join("tools", "frontend"),
-            check=True,
-            shell=platform.system() == "Windows",
-        )

+ 67 - 90
tools/release/setup_version.py

@@ -8,117 +8,94 @@
 # 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.
+# --------------------------------------------------------------------------------------------------
+# Checks that build version matches package version.
+# Updates version number for future 'dev' builds.
+#
+# Invoked from the workflow in build-and-release.yml.
+#
+# Outputs a line for each package (all packages if 'all'):
+#   <package_short_name>_VERSION=<package_version>
+# If a dev release is requested, a similar line is issued indicating the next dev version number:
+#   NEXT_<package_short_name>_VERSION=<package_version>
+# --------------------------------------------------------------------------------------------------
 
 import json
-import os
-import re
 import sys
-from dataclasses import asdict, dataclass
-from typing import Optional
-
-
-@dataclass
-class Version:
-    major: str
-    minor: str
-    patch: str
-    ext: Optional[str] = None
-
-    def bump_ext_version(self) -> None:
-        if not self.ext:
-            return
-        reg = re.compile(r"[0-9]+$")
-        num = reg.findall(self.ext)[0]
-
-        self.ext = self.ext.replace(num, str(int(num) + 1))
-
-    def validate_suffix(self, suffix="dev"):
-        if suffix not in self.ext:
-            raise Exception(f"Version does not contain suffix {suffix}")
-
-    @property
-    def name(self) -> str:
-        """returns a string representation of a version"""
-        return f"{self.major}.{self.minor}.{self.patch}"
-
-    @property
-    def dev_name(self) -> str:
-        """returns a string representation of a version"""
-        return f"{self.name}.{self.ext}"
-
-    def __str__(self) -> str:
-        """returns a string representation of a version"""
-        version_str = f"{self.major}.{self.minor}.{self.patch}"
-        if self.ext:
-            version_str = f"{version_str}.{self.ext}"
-        return version_str
-
-
-def __load_version_from_path(base_path: str) -> Version:
-    """Load version.json file from base path."""
-    with open(os.path.join(base_path, "version.json")) as version_file:
-        data = json.load(version_file)
-        return Version(**data)
+from pathlib import Path
 
+from common import PACKAGES, Package, Version, fetch_latest_github_taipy_releases
 
-def __write_version_to_path(base_path: str, version: Version) -> None:
-    with open(os.path.join(base_path, "version.json"), "w") as version_file:
-        json.dump(asdict(version), version_file)
 
+def usage() -> None:
+    print(f"Usage: {sys.argv[0]} <package> <release_type> [<version> <branch>]")  # noqa: T201
+    print("   if <package> is 'ALL' then all packages including taipy are processed.")  # noqa: T201
+    print("   <release_type> must be 'dev' or 'production'")  # noqa: T201
+    print("     If <release_type> is 'production', <version> must be specified as the target")  # noqa: T201
+    print("     version and <branch> must be set to the name of the current branch.")  # noqa: T201
 
-def extract_version(base_path: str) -> Version:
+
+def extract_version(package: Package) -> Version:
     """
-    Load version.json file from base path and return the version string.
+    Returns a Version from a package's version.json content.
     """
-    return __load_version_from_path(base_path)
-
+    with open(Path(package.package_dir) / "version.json") as version_file:
+        data = json.load(version_file)
+        return Version(**data)
 
-def __setup_dev_version(version: Version, _base_path: str, name: Optional[str] = None) -> None:
-    version.validate_suffix()
 
-    name = f"{name}_VERSION" if name else "VERSION"
-    print(f"{name}={version.dev_name}")  # noqa: T201
+def __setup_dev_version(package: Package, version: Version) -> None:
+    if not version.validate_extension("dev"):
+        raise ValueError(f"{version=} is not a 'dev' version in package {package}.")
 
-    version.bump_ext_version()
+    var_name = f"{package.short_name}_VERSION"
+    print(f"{var_name}={version.full_name}")  # noqa: T201
 
-    __write_version_to_path(_base_path, version)
-    print(f"NEW_{name}={version.dev_name}")  # noqa: T201
+    # Increment dev version
+    version = version.bump_ext_version()
+    # Save in packages's version.json
+    with open(Path(package.package_dir) / "version.json", "w") as version_file:
+        json.dump(version.to_dict(), version_file)
+    # Return it to the GitHub step
+    print(f"NEXT_{var_name}={version.full_name}")  # noqa: T201
 
 
-def __setup_prod_version(version: Version, target_version: str, branch_name: str, name: str = None) -> None:
-    if str(version) != target_version:
-        raise ValueError(f"Current version={version} does not match target version={target_version}")
+def __setup_prod_version(
+    package: Package, version: Version, target_version: str, branch_name: str) -> None:
+    if version.full_name != target_version:
+        raise ValueError(f"Current {version=} does not match {target_version=} for package {package.name}")
 
+    # Production releases can only be performed from a release branch
     if target_branch_name := f"release/{version.major}.{version.minor}" != branch_name:
         raise ValueError(
             f"Branch name mismatch branch={branch_name} does not match target branch name={target_branch_name}"
         )
 
-    name = f"{name}_VERSION" if name else "VERSION"
-    print(f"{name}={version.name}")  # noqa: T201
+    print(f"{package.short_name}_VERSION={version.name}")  # noqa: T201
 
 
 if __name__ == "__main__":
-    paths = (
-        [sys.argv[1]]
-        if sys.argv[1] != "ALL"
-        else [
-            f"taipy{os.sep}common",
-            f"taipy{os.sep}core",
-            f"taipy{os.sep}rest",
-            f"taipy{os.sep}gui",
-            f"taipy{os.sep}templates",
-            "taipy",
-        ]
-    )
-    _environment = sys.argv[2]
-
-    for _path in paths:
-        _version = extract_version(_path)
-        _name = None if _path == "taipy" else _path.split(os.sep)[-1]
-
-        if _environment == "dev":
-            __setup_dev_version(_version, _path, _name)
-
-        if _environment == "production":
-            __setup_prod_version(_version, sys.argv[3], sys.argv[4], _name)
+    if len(sys.argv) < 3:
+        usage()
+        raise ValueError("Missing arguments.")
+
+    packages = [sys.argv[1]] if sys.argv[1].lower() != "all" else PACKAGES + ["taipy"]
+    release_type = sys.argv[2]
+
+    for package_name in packages:
+        package = Package(package_name)
+        version = extract_version(package)
+
+        if release_type == "dev":
+            __setup_dev_version(package, version)
+        elif release_type == "production":
+            if len(sys.argv) < 5:
+                usage()
+                raise ValueError("Missing arguments.")
+            __setup_prod_version(package, version, sys.argv[3], sys.argv[4])
+        else:
+            usage()
+            raise ValueError(f"Invalid <release_type> argument ({release_type}).")
+
+    # Print out the latest 'taipy' version that has no extension
+    print(f"LATEST_TAIPY_VERSION={fetch_latest_github_taipy_releases()}")  # noqa: T201

+ 0 - 43
tools/release/update_setup.py

@@ -1,43 +0,0 @@
-# Copyright 2021-2024 Avaiga Private Limited
-#
-# Licensed 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.
-
-import sys
-
-
-def update_setup() -> None:
-    with open("setup.taipy.py", mode="r") as setup_r, open("setup.py", mode="w") as setup_w:
-        in_requirements = False
-        looking = True
-        for line in setup_r:
-            if looking:
-                if line.lstrip().startswith("requirements") and line.rstrip().endswith("["):
-                    in_requirements = True
-                elif in_requirements:
-                    if line.strip() == "]":
-                        looking = False
-                    else:
-                        if line.lstrip().startswith('"taipy-gui@git+https'):
-                            start = line.find('"taipy-gui')
-                            end = line.rstrip().find(",")
-                            line = f'{line[:start]}"taipy-gui=={sys.argv[1]}"{line[end:]}'
-                        elif line.lstrip().startswith('"taipy-rest@git+https'):
-                            start = line.find('"taipy-rest')
-                            end = line.rstrip().find(",")
-                            line = f'{line[:start]}"taipy-rest=={sys.argv[2]}"{line[end:]}'
-                        elif line.lstrip().startswith('"taipy-templates@git+https'):
-                            start = line.find('"taipy-templates')
-                            end = line.rstrip().find(",")
-                            line = f'{line[:start]}"taipy-templates=={sys.argv[3]}"{line[end:]}'
-            setup_w.write(line)
-
-
-if __name__ == "__main__":
-    update_setup()

+ 68 - 25
tools/release/update_setup_requirements.py

@@ -8,45 +8,88 @@
 # 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.
+# --------------------------------------------------------------------------------------------------
+# Updates the setup.requirements.txt files for a given package.
+#
+# Invoked by workflows/build-and-release-single-package.yml and workflows/build-and-release.yml.
+# Working directory must be [root_dir].
+# --------------------------------------------------------------------------------------------------
 
 import os
+import re
 import sys
-from typing import Dict
+import typing as t
+
+from common import PACKAGES, Version, retrieve_github_path
 
 BASE_PATH = "./tools/packages"
 
 
-def __build_taipy_package_line(line: str, version: str, publish_on_py_pi: bool) -> str:
-    _line = line.strip()
-    if publish_on_py_pi:
-        return f"{_line}=={version}\n"
-    tag = f"{version}-{_line.split('-')[1]}"
-    tar_name = f"{_line}-{version}"
-    return f"{_line} @ https://github.com/Avaiga/taipy/releases/download/{tag}/{tar_name}.tar.gz\n"
+def usage() -> None:
+    packages = "> <".join(f"{p}_ver" for p in PACKAGES)
+    print(  # noqa: T201
+        f"Usage: {sys.argv[0]} <package> <{packages}> <deps> [<gh_path>]"
+    )
+    packages = ", ".join(f"'{p}'" for p in PACKAGES[:-1])
+    packages = f"{packages}, or '{PACKAGES[-1]}'"
+    print(f"   <package> must be one of {packages}.")  # noqa: T201
+    for p in PACKAGES:
+        print(f"   <{p}_ver>: minimal version of the taipy-{p} dependency.")  # noqa: T201
+    print("   <deps> must be 'Pypi' or 'GitHub', indicating where to find Taipy package dependencies.")  # noqa: T201
+    print("   <gh_path>: The path of GitHub repository (owner/repo), used if <deps> is 'GitHub'.")  # noqa: T201
+
 
+def __build_taipy_package_line(line: str, version: Version, use_pypi: bool, gh_path: t.Optional[str]) -> str:
+    line = line.strip()
+    if use_pypi:
+        # Target dependency version should the latest compatible with 'version'
+        return f"{line} >={version.major}.{version.minor},<{version.major}.{version.minor + 1}\n"
+    tag = f"{version}-{line.split('-')[1]}"
+    tar_name = f"{line}-{version}"
+    return f"{line} @ https://github.com/{gh_path}/releases/download/{tag}/{tar_name}.tar.gz\n"
 
-def update_setup_requirements(package: str, versions: Dict, publish_on_py_pi: bool) -> None:
-    _path = os.path.join(BASE_PATH, package, "setup.requirements.txt")
+
+def update_setup_requirements(
+    package: str, versions: dict[str, Version], publish_on_py_pi: bool, gh_path: t.Optional[str]
+) -> None:
+    path = os.path.join(BASE_PATH, "taipy" if package == "taipy" else f"taipy-{package}", "setup.requirements.txt")
     lines = []
-    with open(_path, mode="r") as req:
+    with open(path, mode="r") as req:
         for line in req:
-            if v := versions.get(line.strip()):
-                line = __build_taipy_package_line(line, v, publish_on_py_pi)
+            if match := re.match(r"^taipy(:?\-\w+)?\s*", line, re.MULTILINE):
+                # Add subpackage version if not forced
+                if not line[match.end() :] and (v := versions.get(line.strip())):
+                    if v == Version.UNKNOWN:
+                        raise ValueError(f"Missing version for dependency '{line.strip()}'.")
+                    line = __build_taipy_package_line(line, v, publish_on_py_pi, gh_path)
             lines.append(line)
 
-    with open(_path, "w") as file:
+    with open(path, "w") as file:
         file.writelines(lines)
+    # Issue the generated files for logging information
+    print(f"Generated setup.requirements.txt for package '{package}'")  # noqa: T201
+    for line in lines:
+        print(line.strip())  # noqa: T201
+    print("-" * 32)  # noqa: T201
 
 
 if __name__ == "__main__":
-    _package = sys.argv[1]
-    _versions = {
-        "taipy-common": sys.argv[2],
-        "taipy-core": sys.argv[3],
-        "taipy-gui": sys.argv[4],
-        "taipy-rest": sys.argv[5],
-        "taipy-templates": sys.argv[6],
-    }
-    _publish_on_py_pi = True if sys.argv[7] == "true" else False
-
-    update_setup_requirements(_package, _versions, _publish_on_py_pi)
+    if len(sys.argv) < len(PACKAGES) + 3:
+        usage()
+        raise ValueError("Missing arguments.")
+    package = sys.argv[1]
+    # Store the provided version for each package listed in PACKAGES
+    versions = {f"taipy-{p}": Version.from_string(sys.argv[i]) for i, p in enumerate(PACKAGES, 2)}
+    # Keep compatibility with legacy actions ('true' is equivalent to 'Pypi')
+    pypi_deps = sys.argv[len(PACKAGES) + 2].lower() in ["true", "pypi"]
+    gh_path = None
+    if not pypi_deps:
+        if len(sys.argv) < len(PACKAGES) + 4:
+            gh_path = retrieve_github_path()
+            if gh_path is None:
+                usage()
+                raise ValueError("Couldn't figure out GitHub branch path.")
+        else:
+            gh_path = sys.argv[len(PACKAGES) + 3]
+
+    update_setup_requirements(package, versions, pypi_deps, gh_path)

+ 14 - 1
tools/validate_taipy_install.py

@@ -8,6 +8,16 @@
 # 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.
+# --------------------------------------------------------------------------------------------------
+# Tests that Taipy is properly installed.
+# - Importing the taipy package exposes crucial attributes.
+# - The taipy-gui package is built
+# - The gui_core extension library is built
+#
+# Exits with error code on failure.
+#
+# Invoked from the workflows in workflows/packaging.yml and workflows/publish.yml.
+# --------------------------------------------------------------------------------------------------
 
 import logging
 import os
@@ -16,7 +26,7 @@ import sys
 
 def test_import_taipy_packages() -> bool:
     """
-    Import taipy package and call gui, Scenario and rest attributes.
+    Import taipy package and check some attributes.
     """
     import taipy as tp
 
@@ -35,6 +45,9 @@ def test_import_taipy_packages() -> bool:
 
 
 def is_taipy_gui_install_valid() -> bool:
+    """
+    Check crucial Taipy GUI build outcomes.
+    """
     from pathlib import Path
 
     import taipy