Browse Source

Merge pull request #477 from Avaiga/feature/merge-rest

Merge Taipy Rest on Taipy
João André 1 năm trước cách đây
mục cha
commit
a842c6668f
100 tập tin đã thay đổi với 7078 bổ sung1 xóa
  1. 1 1
      src/taipy/_run.py
  2. 2 0
      src/taipy/rest/.coveragerc
  3. 4 0
      src/taipy/rest/.dockerignore
  4. 24 0
      src/taipy/rest/.flake8
  5. 4 0
      src/taipy/rest/.flaskenv
  6. 38 0
      src/taipy/rest/.github/workflows/codeql-analysis.yml
  7. 27 0
      src/taipy/rest/.github/workflows/coverage.yml
  8. 89 0
      src/taipy/rest/.github/workflows/publish.yml
  9. 141 0
      src/taipy/rest/.github/workflows/release-dev.yml
  10. 69 0
      src/taipy/rest/.github/workflows/release.yml
  11. 32 0
      src/taipy/rest/.github/workflows/setuptools.yml
  12. 52 0
      src/taipy/rest/.github/workflows/tests.yml
  13. 141 0
      src/taipy/rest/.gitignore
  14. 9 0
      src/taipy/rest/.isort.cfg
  15. 10 0
      src/taipy/rest/.license-header
  16. 41 0
      src/taipy/rest/.pre-commit-config.yaml
  17. 2 0
      src/taipy/rest/.testenv
  18. 128 0
      src/taipy/rest/CODE_OF_CONDUCT.md
  19. 135 0
      src/taipy/rest/CONTRIBUTING.md
  20. 97 0
      src/taipy/rest/INSTALLATION.md
  21. 21 0
      src/taipy/rest/LICENSE
  22. 1 0
      src/taipy/rest/MANIFEST.in
  23. 28 0
      src/taipy/rest/Pipfile
  24. 88 0
      src/taipy/rest/README.md
  25. 26 0
      src/taipy/rest/__init__.py
  26. 12 0
      src/taipy/rest/_init.py
  27. 14 0
      src/taipy/rest/api/__init__.py
  28. 108 0
      src/taipy/rest/api/error_handler.py
  29. 10 0
      src/taipy/rest/api/exceptions/__init__.py
  30. 25 0
      src/taipy/rest/api/exceptions/exceptions.py
  31. 10 0
      src/taipy/rest/api/middlewares/__init__.py
  32. 34 0
      src/taipy/rest/api/middlewares/_middleware.py
  33. 38 0
      src/taipy/rest/api/resources/__init__.py
  34. 420 0
      src/taipy/rest/api/resources/cycle.py
  35. 662 0
      src/taipy/rest/api/resources/datanode.py
  36. 287 0
      src/taipy/rest/api/resources/job.py
  37. 527 0
      src/taipy/rest/api/resources/scenario.py
  38. 292 0
      src/taipy/rest/api/resources/sequence.py
  39. 278 0
      src/taipy/rest/api/resources/task.py
  40. 43 0
      src/taipy/rest/api/schemas/__init__.py
  41. 25 0
      src/taipy/rest/api/schemas/cycle.py
  42. 101 0
      src/taipy/rest/api/schemas/datanode.py
  43. 27 0
      src/taipy/rest/api/schemas/job.py
  44. 27 0
      src/taipy/rest/api/schemas/scenario.py
  45. 25 0
      src/taipy/rest/api/schemas/sequence.py
  46. 24 0
      src/taipy/rest/api/schemas/task.py
  47. 213 0
      src/taipy/rest/api/views.py
  48. 59 0
      src/taipy/rest/app.py
  49. 10 0
      src/taipy/rest/commons/__init__.py
  50. 103 0
      src/taipy/rest/commons/apispec.py
  51. 28 0
      src/taipy/rest/commons/encoder.py
  52. 50 0
      src/taipy/rest/commons/pagination.py
  53. 22 0
      src/taipy/rest/commons/templates/redoc.j2
  54. 51 0
      src/taipy/rest/commons/templates/swagger.j2
  55. 28 0
      src/taipy/rest/commons/to_from_model.py
  56. 8 0
      src/taipy/rest/contributors.txt
  57. 20 0
      src/taipy/rest/extensions.py
  58. 45 0
      src/taipy/rest/rest.py
  59. 58 0
      src/taipy/rest/setup.py
  60. 10 0
      src/taipy/rest/tests/__init__.py
  61. 315 0
      src/taipy/rest/tests/conftest.py
  62. 9 0
      src/taipy/rest/tests/json/expected/cycle.json
  63. 18 0
      src/taipy/rest/tests/json/expected/datanode.json
  64. 15 0
      src/taipy/rest/tests/json/expected/job.json
  65. 19 0
      src/taipy/rest/tests/json/expected/scenario.json
  66. 13 0
      src/taipy/rest/tests/json/expected/sequence.json
  67. 17 0
      src/taipy/rest/tests/json/expected/task.json
  68. 10 0
      src/taipy/rest/tests/setup/__init__.py
  69. BIN
      src/taipy/rest/tests/setup/my_model.p
  70. 10 0
      src/taipy/rest/tests/setup/shared/__init__.py
  71. 55 0
      src/taipy/rest/tests/setup/shared/algorithms.py
  72. 42 0
      src/taipy/rest/tests/setup/shared/config.py
  73. 62 0
      src/taipy/rest/tests/test_cycle.py
  74. 111 0
      src/taipy/rest/tests/test_datanode.py
  75. 100 0
      src/taipy/rest/tests/test_end_to_end.py
  76. 83 0
      src/taipy/rest/tests/test_job.py
  77. 59 0
      src/taipy/rest/tests/test_middleware.py
  78. 90 0
      src/taipy/rest/tests/test_scenario.py
  79. 102 0
      src/taipy/rest/tests/test_sequence.py
  80. 88 0
      src/taipy/rest/tests/test_task.py
  81. 44 0
      src/taipy/rest/tox.ini
  82. 1 0
      src/taipy/rest/version.json
  83. 22 0
      src/taipy/rest/version.py
  84. 10 0
      tests/rest/__init__.py
  85. 315 0
      tests/rest/conftest.py
  86. 9 0
      tests/rest/json/expected/cycle.json
  87. 18 0
      tests/rest/json/expected/datanode.json
  88. 15 0
      tests/rest/json/expected/job.json
  89. 19 0
      tests/rest/json/expected/scenario.json
  90. 13 0
      tests/rest/json/expected/sequence.json
  91. 17 0
      tests/rest/json/expected/task.json
  92. 10 0
      tests/rest/setup/__init__.py
  93. BIN
      tests/rest/setup/my_model.p
  94. 10 0
      tests/rest/setup/shared/__init__.py
  95. 55 0
      tests/rest/setup/shared/algorithms.py
  96. 42 0
      tests/rest/setup/shared/config.py
  97. 62 0
      tests/rest/test_cycle.py
  98. 111 0
      tests/rest/test_datanode.py
  99. 100 0
      tests/rest/test_end_to_end.py
  100. 83 0
      tests/rest/test_job.py

+ 1 - 1
src/taipy/_run.py

@@ -57,7 +57,7 @@ def _run(*services: _AppType, **kwargs) -> t.Optional[Flask]:
         return None
 
     if gui and rest:
-        gui._set_flask(rest._app)
+        gui._set_flask(rest._app)  # type: ignore
         return gui.run(**kwargs)
     else:
         app = rest or gui

+ 2 - 0
src/taipy/rest/.coveragerc

@@ -0,0 +1,2 @@
+[run]
+omit = taipy_rest/setup/*

+ 4 - 0
src/taipy/rest/.dockerignore

@@ -0,0 +1,4 @@
+.flaskenv
+Dockerfile
+Makefile
+docker-compose.yml

+ 24 - 0
src/taipy/rest/.flake8

@@ -0,0 +1,24 @@
+[flake8]
+# required by black, https://github.com/psf/black/blob/master/.flake8
+max-line-length = 120
+max-complexity = 18
+ignore = E203, E266, E501, E722, W503, F403, F401
+select = B,C,E,F,W,T4,B9
+docstring-convention = google
+per-file-ignores =
+    __init__.py:F401
+exclude =
+    .git,
+    __pycache__,
+    setup.py,
+    build,
+    dist,
+    releases,
+    .venv,
+    .tox,
+    .mypy_cache,
+    .pytest_cache,
+    .vscode,
+    .github,
+    tests,
+    tests/setup

+ 4 - 0
src/taipy/rest/.flaskenv

@@ -0,0 +1,4 @@
+FLASK_ENV=development
+FLASK_APP=src.taipy.rest.app:create_app
+SECRET_KEY=xqgl3(t0und)yca(kij6uux!wse0j5i15!zy9^v(#p8^b-22#8
+DATABASE_URI=sqlite:///myapi.db

+ 38 - 0
src/taipy/rest/.github/workflows/codeql-analysis.yml

@@ -0,0 +1,38 @@
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ develop, dev/*, release/* ]
+  pull_request:
+    branches: [ develop, dev/*, release/* ]
+  schedule:
+    - cron: '41 4 * * 0'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2

+ 27 - 0
src/taipy/rest/.github/workflows/coverage.yml

@@ -0,0 +1,27 @@
+name: 'code-coverage'
+on:
+  pull_request:
+    branches: [ develop, dev/*, release/* ]
+  workflow_dispatch:
+
+jobs:
+  backend-code-coverage:
+    timeout-minutes: 10
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python 3.9
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.9
+
+      - name: Lint and Coverage
+        run: |
+          pip install tox
+          tox
+
+      - name: Code coverage
+        uses: orgoro/coverage@v2
+        with:
+          coverageFile: coverage.xml
+          token: ${{ secrets.GITHUB_TOKEN }}

+ 89 - 0
src/taipy/rest/.github/workflows/publish.yml

@@ -0,0 +1,89 @@
+name: Publish on Pypi
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "The tag of the package to publish on Pypi (ex: 1.0.0, 1.0.0.dev0)"
+        required: true
+
+jobs:
+  test-package:
+    timeout-minutes: 20
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v2
+        with:
+          python-version: 3.8
+
+      - name: Extract Github Tag Version
+        id: vars
+        run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
+
+      - name: Ensure package version is properly set
+        run: |
+          echo """
+          import json, sys, os
+          with open(f\"src{os.sep}taipy{os.sep}rest{os.sep}version.json\") as version_file:
+            version_o = json.load(version_file)
+          version = f'{version_o.get(\"major\")}.{version_o.get(\"minor\")}.{version_o.get(\"patch\")}'
+          if vext := version_o.get(\"ext\"):
+            version = f'{version}.{vext}'
+          if version != sys.argv[1]:
+            raise ValueError(f\"Invalid version {version} / {sys.argv[1]}\")
+          if sys.argv[1] != sys.argv[2]:
+            raise ValueError(f\"Invalid tag version {sys.argv[2]} with package version {sys.argv[1]}\")
+          """ > /tmp/check.py
+          python /tmp/check.py "${{ github.event.inputs.version }}" "${{ steps.vars.outputs.tag }}"
+
+      - name: Download assets from github release tag
+        run: |
+          gh release download ${{ github.event.inputs.version }} --dir dist
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Verify there is a release asset
+        run: |
+          if [ ! -f dist/${{ github.event.repository.name }}-${{ github.event.inputs.version }}.tar.gz ]; then
+            echo "No release asset found"
+            exit 1
+          fi
+
+  publish-to-pypi:
+    needs: [test-package]
+    timeout-minutes: 20
+    environment: publish
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Download assets from tag
+        run: |
+          gh release download ${{ github.event.inputs.version }} --dir dist
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Publish to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
+
+  test-published-package:
+    needs: [publish-to-pypi]
+    timeout-minutes: 5
+    strategy:
+      matrix:
+        python-versions: ['3.8','3.9','3.10']
+        os: [ubuntu-latest,windows-latest,macos-latest]
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-versions }}
+
+      - name: Install and test package
+        run: |
+          pip install --no-cache-dir ${{ github.event.repository.name }}==${{ github.event.inputs.version }}

+ 141 - 0
src/taipy/rest/.github/workflows/release-dev.yml

@@ -0,0 +1,141 @@
+name: Create Github Dev Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      taipy-core-version:
+        description: "The taipy-core version to use (ex: 2.3.0.dev0)"
+
+jobs:
+  release-dev-package:
+    timeout-minutes: 20
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          ssh-key: ${{secrets.DEPLOY_KEY}}
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: 3.8
+
+      - name: Ensure package version has 'dev' suffix
+        run: |
+          echo """
+          import json, sys, os
+          SUFFIX = 'dev'
+          with open(f\"src{os.sep}taipy{os.sep}rest{os.sep}version.json\") as version_file:
+              version_o = json.load(version_file)
+          version = f'{version_o.get(\"major\")}.{version_o.get(\"minor\")}.{version_o.get(\"patch\")}'
+          if vext := version_o.get(\"ext\"):
+              version = f'{version}.{vext}'
+          if SUFFIX not in version:
+              raise ValueError(f\"version {version} does not contain suffix {SUFFIX}\")
+          """ > /tmp/check1.py
+          python /tmp/check1.py
+
+      - name: Extract package version
+        id: current-version
+        run: |
+          echo """
+          import json, os
+          with open(f\"src{os.sep}taipy{os.sep}rest{os.sep}version.json\") as version_file:
+              version_o = json.load(version_file)
+          version = f'{version_o.get(\"major\")}.{version_o.get(\"minor\")}.{version_o.get(\"patch\")}'
+          if vext := version_o.get(\"ext\"):
+              version = f'{version}.{vext}'
+          print(f'VERSION={version}')
+          """ > /tmp/check2.py
+          python /tmp/check2.py >> $GITHUB_OUTPUT
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install build
+
+      - name: Check dependencies are available
+        if: github.event.inputs.taipy-core-version != ''
+        run: |
+          curl https://pypi.org/simple/taipy-core/ | grep -o ">taipy-core-${{ github.event.inputs.taipy-core-version }}\.tar\.gz<"
+
+      - name: Update setup.py locally
+        if: github.event.inputs.taipy-core-version != ''
+        run: |
+          mv setup.py setup.taipy.py
+          echo """
+          import sys
+          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('install_requires') and line.rstrip().endswith('['):
+                          in_requirements = True
+                      elif in_requirements:
+                          if line.strip() == '],':
+                              looking = False
+                          else:
+                              if line.lstrip().startswith('\"taipy-core@git+https'):
+                                  start = line.find('\"taipy-core')
+                                  end = line.rstrip().find(',')
+                                  line = f'{line[:start]}\"taipy-core=={sys.argv[1]}\"{line[end:]}'
+                  setup_w.write(line)
+          """ > /tmp/write_setup_taipy.py
+          python /tmp/write_setup_taipy.py "${{ github.event.inputs.taipy-core-version }}"
+
+      - name: Build package
+        run: python setup.py build_py && python -m build
+
+      - name: Install the package and test it
+        run: |
+          # Install package
+          echo "Installing package..."
+          pip install ./dist/${{ github.event.repository.name }}-${{ steps.current-version.outputs.VERSION }}.tar.gz
+
+          # Run tests
+          python -c "import taipy as tp; tp.rest"
+
+      - name: Extract commit hash
+        shell: bash
+        run: echo "##[set-output name=hash;]$(echo $(git rev-parse HEAD))"
+        id: extract_hash
+
+      - name: Create/update release and tag
+        run: |
+          echo "Creating release ${{ steps.current-version.outputs.VERSION }}"
+          gh release create ${{ steps.current-version.outputs.VERSION }} ./dist/${{ github.event.repository.name }}-${{ steps.current-version.outputs.VERSION }}.tar.gz --target ${{ steps.extract_hash.outputs.hash }} --prerelease --title ${{ steps.current-version.outputs.VERSION }} --notes "Dev Release ${{ steps.current-version.outputs.VERSION }}"
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+      - name: Reset changes
+        run: |
+          git reset --hard HEAD
+          git clean -fdx
+
+      - name: Increase dev version
+        id: new-version
+        run: |
+          echo """
+          import json, os
+          with open(f'src{os.sep}taipy{os.sep}rest{os.sep}version.json') as version_file:
+              version_o = json.load(version_file)
+              if version_o is None or 'dev' not in version_o['ext']:
+                  raise ValueError('Invalid version file. Version must contain dev suffix.')
+              prev_version = version_o['ext']
+              new_version = 'dev' + str(int(version_o['ext'].replace('dev', '')) + 1)
+              with open(f'src{os.sep}taipy{os.sep}rest{os.sep}version.json') as r:
+                  text = r.read().replace(prev_version, new_version)
+              with open(f'src{os.sep}taipy{os.sep}rest{os.sep}version.json', mode='w') as w:
+                  w.write(text)
+              with open(f\"src{os.sep}taipy{os.sep}rest{os.sep}version.json\") as version_file:
+                  version_o = json.load(version_file)
+              version = f'{version_o.get(\"major\")}.{version_o.get(\"minor\")}.{version_o.get(\"patch\")}'
+              if vext := version_o.get(\"ext\"):
+                  version = f'{version}.{vext}'
+              print(f'VERSION={version}')
+          """ > /tmp/increase_dev_version.py
+          python /tmp/increase_dev_version.py >> $GITHUB_OUTPUT
+
+      - uses: stefanzweifel/git-auto-commit-action@v4
+        with:
+          commit_message: Update version to ${{ steps.new-version.outputs.VERSION }}

+ 69 - 0
src/taipy/rest/.github/workflows/release.yml

@@ -0,0 +1,69 @@
+name: Create Github Release
+
+on:
+  workflow_dispatch:
+    inputs:
+      version:
+        description: "The release/package version to create (ex: 1.0.0)"
+        required: true
+
+jobs:
+  release-package:
+    timeout-minutes: 20
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/setup-python@v2
+        with:
+          python-version: 3.8
+
+      - name: Extract branch name
+        shell: bash
+        run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
+        id: extract_branch
+
+      - name: Ensure package version is properly set
+        run: |
+          echo """
+          import json, sys, os
+          with open(f\"src{os.sep}taipy{os.sep}rest{os.sep}version.json\") as version_file:
+              version_o = json.load(version_file)
+          version = f'{version_o.get(\"major\")}.{version_o.get(\"minor\")}.{version_o.get(\"patch\")}'
+          if vext := version_o.get(\"ext\"):
+              version = f'{version}.{vext}'
+          if version != sys.argv[1]:
+              raise ValueError(f\"Invalid version {version} / {sys.argv[1]}\")
+          """ > /tmp/check1.py
+          python /tmp/check1.py "${{ github.event.inputs.version }}"
+
+      - name: Validate branch name
+        run: |
+          echo """
+          import json, sys, os
+          with open(f\"src{os.sep}taipy{os.sep}rest{os.sep}version.json\") as version_file:
+              version = json.load(version_file)
+          if f'release/{version.get(\"major\")}.{version.get(\"minor\")}' != sys.argv[1]:
+              raise ValueError(f'Branch name mismatch: release/{version.get(\"major\")}.{version.get(\"minor\")} != {sys.argv[1]}')
+          """ > /tmp/check2.py
+          python /tmp/check2.py "${{ steps.extract_branch.outputs.branch }}"
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install build
+
+      - name: Build and test the package
+        run: |
+          python setup.py build_py && python -m build
+          pip install dist/*.tar.gz
+
+      - name: Extract commit hash
+        shell: bash
+        run: echo "##[set-output name=hash;]$(echo $(git rev-parse HEAD))"
+        id: extract_hash
+
+      - name: Create/update release and tag
+        run: |
+            gh release create ${{ github.event.inputs.version }} ./dist/${{ github.event.repository.name }}-${{ github.event.inputs.version }}.tar.gz --target ${{ steps.extract_hash.outputs.hash }} --title ${{ github.event.inputs.version }}
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 32 - 0
src/taipy/rest/.github/workflows/setuptools.yml

@@ -0,0 +1,32 @@
+name: Test package installation
+
+on:
+  push:
+    branches: [ develop, dev/*, release/* ]
+  pull_request:
+    branches: [ develop, dev/*, release/* ]
+  schedule:
+    - cron: "0 8 * * *"
+
+jobs:
+  standard-packages:
+    timeout-minutes: 10
+    strategy:
+      matrix:
+        python-versions: [ '3.8', '3.9', '3.10', '3.11']
+        os: [ ubuntu-latest, windows-latest, macos-latest ]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-versions }}
+
+      - name: Test packaging
+        run: |
+          pip install .
+
+          python -c "import taipy as tp; tp.Scenario"
+          python -c "import taipy as tp; tp.rest"

+ 52 - 0
src/taipy/rest/.github/workflows/tests.yml

@@ -0,0 +1,52 @@
+name: test workflow
+
+on:
+  push:
+    branches: [ develop, dev/*, release/* ]
+  pull_request:
+    branches: [ develop, dev/*, release/* ]
+
+  workflow_dispatch:
+    inputs:
+      user-to-notify:
+        description: "Github username to notify"
+        required: false
+        default: ""
+
+jobs:
+  test:
+    timeout-minutes: 30
+    strategy:
+      matrix:
+        python-versions: ['3.8', '3.9', '3.10', '3.11']
+        os: [ubuntu-latest, windows-latest, macos-latest]
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-versions }}
+
+      - name: Tests
+        run: |
+          pip install tox
+          tox -e tests
+
+      - name: Ensure no usage of manager classes without factory
+        if: matrix.python-versions == '3.10' && matrix.os == 'ubuntu-latest'
+        run: |
+          ! grep -rP '_ScenarioManager(?!Factory)' src
+          ! grep -rP '_DataManager(?!Factory)' src
+          ! grep -rP '_TaskManager(?!Factory)' src
+          ! grep -rP '_SequenceManager(?!Factory)' src
+          ! grep -rP '_JobManager(?!Factory)' src
+          ! grep -rP '_CycleManager(?!Factory)' src
+
+      - name: Notify user if failed
+        if: failure() && github.event_name == 'workflow_dispatch'
+        run: |
+          if [[ -n "${{ github.event.inputs.user-to-notify }}" ]]; then
+            curl "${{ secrets.notify_endpoint }}" -d '{"username": "${{ github.event.inputs.user-to-notify }}", "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" }' -H "Content-Type: application/json"
+          fi
+        shell: bash

+ 141 - 0
src/taipy/rest/.gitignore

@@ -0,0 +1,141 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+Pipfile.lock
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# GUI generation
+taipy_webapp/
+# Doc generation
+docs/gui/controls.md
+docs/gui/controls/*.md
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# dotenv
+*.env
+
+# virtualenv
+.venv
+venv/
+ENV/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+# IDE settings
+.vscode/
+.idea/
+.idea/taipy.iml
+.DS_Store
+
+# mkdocs build dir
+site/
+
+# Demo Testing File
+demo*.py
+*.csv
+demo*.css
+object_selection.py
+gui_assets
+dataset
+
+# Demos folder
+demo*/
+!demos/
+
+# Docker local dev
+docker-compose-dev*.yml
+Dockerfile.dev
+
+# Filesystem default local storage
+.data/
+.databkp/
+
+*.db
+src/taipy/rest/setup/*

+ 9 - 0
src/taipy/rest/.isort.cfg

@@ -0,0 +1,9 @@
+[settings]
+multi_line_output = 3
+include_trailing_comma = True
+force_grid_wrap = 0
+use_parentheses = True
+ensure_newline_before_comments = True
+line_length = 120
+# you can skip files as below
+#skip_glob = docs/conf.py

+ 10 - 0
src/taipy/rest/.license-header

@@ -0,0 +1,10 @@
+Copyright 2023 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.

+ 41 - 0
src/taipy/rest/.pre-commit-config.yaml

@@ -0,0 +1,41 @@
+repos:
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: "v0.910" # Use the sha / tag you want to point at
+    hooks:
+      - id: mypy
+        additional_dependencies: ["types-Markdown", "types-python-dateutil", "types-pytz", "types-tzlocal"]
+  - repo: https://github.com/Lucas-C/pre-commit-hooks
+    rev: v1.1.13
+    hooks:
+      - id: forbid-crlf
+      - id: remove-crlf
+      - id: forbid-tabs
+      - id: remove-tabs
+      - id: insert-license
+        files: \.py$
+        args:
+          - --license-filepath
+          - .license-header
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.0.1
+    hooks:
+      - id: trailing-whitespace
+      - id: end-of-file-fixer
+      - id: check-merge-conflict
+      - id: check-yaml
+        args: [--unsafe]
+  - repo: https://github.com/pre-commit/mirrors-isort
+    rev: v5.9.3
+    hooks:
+      - id: isort
+  - repo: https://github.com/ambv/black
+    rev: 22.3.0
+    hooks:
+      - id: black
+        args: [--line-length=120]
+        language_version: python3
+  - repo: https://gitlab.com/pycqa/flake8
+    rev: 3.9.2
+    hooks:
+      - id: flake8
+        additional_dependencies: [flake8-typing-imports==1.10.0]

+ 2 - 0
src/taipy/rest/.testenv

@@ -0,0 +1,2 @@
+SECRET_KEY=testing
+DATABASE_URI=sqlite:///:memory:

+ 128 - 0
src/taipy/rest/CODE_OF_CONDUCT.md

@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people.
+* Being respectful of differing opinions, viewpoints, and experiences.
+* Giving and gracefully accepting constructive feedback.
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience.
+* Focusing on what is best not just for us as individuals, but for the
+  overall community.
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+  advances of any kind.
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment.
+* Publishing others' private information, such as a physical or email
+  address, without their explicit permission.
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting.
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+rnd@avaiga.com.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior,  harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.

+ 135 - 0
src/taipy/rest/CONTRIBUTING.md

@@ -0,0 +1,135 @@
+# Contributions
+
+Thanks for your interest in helping improve Taipy! Contributions are welcome, and they are greatly appreciated!
+Every little help and credit will always be given.
+
+There are multiple ways to contribute to Taipy: code, but also reporting bugs, creating feature requests, helping
+other users in our forums, [stack**overflow**](https://stackoverflow.com/), etc.
+
+Today the only way to communicate with the Taipy team is by GitHub issues.
+
+## Never contributed on an open source project before ?
+
+Have a look on this [GitHub documentation](https://docs.github.com/en/get-started/quickstart/contributing-to-projects).
+
+## Report bugs
+
+Reporting bugs is through [GitHub issues](https://github.com/Avaiga/taipy/issues).
+
+Please report relevant information and preferably code that exhibits the problem. We provide templates to help you
+describe the issue.
+
+The Taipy team will analyse and try to reproduce the bug to provide feedback. If confirmed, we will add a priority
+to the issue and add it in our backlog. Feel free to propose a pull request to fix it.
+
+## Issue reporting, feedback, proposal, design or any other comment
+
+Any feedback or proposal is greatly appreciated! Do not hesitate to create an issue with the appropriate template on
+[GitHub](https://github.com/Avaiga/taipy/issues).
+
+The Taipy team will analyse your issue and return to you as soon as possible.
+
+## Improve Documentation
+
+Do not hesitate to create an issue or pull request directly on the
+[taipy-doc repository](https://github.com/Avaiga/taipy-doc).
+
+## Implement Features
+
+The Taipy team manages its backlog in private. Each issue that will be done during our current sprint is
+attached to the `current sprint`. Please, do not work on it, the Taipy team is on it.
+
+## Code organisation
+
+Taipy is organised in five main repositories:
+
+- [taipy-config](https://github.com/Avaiga/taipy-config).
+- [taipy-core](https://github.com/Avaiga/taipy-core).
+- [taipy-gui](https://github.com/Avaiga/taipy-gui).
+- [taipy-rest](https://github.com/Avaiga/taipy-rest).
+- [taipy](https://github.com/Avaiga/taipy) brings previous packages in a single one.
+
+## Coding style and best practices
+
+### Python
+
+Taipy's repositories follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) and
+[PEP 484](https://www.python.org/dev/peps/pep-0484/) coding convention.
+
+## TypeScript
+
+Taipy's repositories use the [ESLint](https://eslint.org/) and
+[TypeScript ESLint](https://github.com/typescript-eslint/typescript-eslint) plugin to ensure a common set of rules.
+
+### Git branches
+
+All new development happens in the `develop` branch. All pull requests should target that branch.
+We are following a strict branch naming convention based on the pattern: `<type>/#<issueId>[IssueSummary]`.
+
+Where:
+
+- `<type>` would be one of:
+    - feature: new feature implementation, or improvement of a feature.
+    - bug: bug fix.
+    - review: change provoked by review comment not immediately taken care of.
+    - refactor: refactor of a piece of code.
+    - doc: doc changes (complement or typo fixes…).
+    - build: in relation with the build process.
+- `<issueId>` is the processed issue identifier. The advantage of explicitly indicating the issue number is that in
+  GitHub, a pull request page shows a direct link to the issue description.
+- `[IssueSummary]` is a short summary of the issue topic, not including spaces, using Camel case or lower-case,
+  dash-separated words. This summary, with its dash (‘-’) symbol prefix, is optional.
+
+
+## Contribution workflow
+
+Find an issue without the label `current sprint` and add a comment on it to inform the community that you are
+working on it.
+
+1. Make your [own fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) of the repository
+   target by the issue. Clone it on our local machine, then go inside the directory.
+
+2. We are working with [Pipenv](https://github.com/pypa/pipenv) for our virtualenv.
+   Create a local env and install development package by running `pipenv install --dev`, then run tests with `pipenv
+   run pytest` to verify your setup.
+
+3. For convention help, we provide a [pre-commit](https://pre-commit.com/hooks.html) file.
+   This tool will run before each commit and will automatically reformat code or raise warnings and errors based on the
+   code format or Python typing.
+   You can install and setup it up by doing:
+   ```
+     pipenv install pre-commit
+     pipenv run python -m pre-commit install
+   ```
+
+4. Make the changes.<br/>
+   You may want to also add your GitHub login as a new line of the `contributors.txt` file located at the root
+   of this repository. We are using it to list our contributors in the Taipy documentation
+   (see the "Contributing > Contributors" section) and thank them.
+
+5. Create a [pull request from your fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork).<br/>
+   Keep your pull request as __draft__ until your work is finished.
+   Do not hesitate to add a comment for help or questions.
+   Before you submit a pull request for review from your forked repo, check that it meets these guidelines:
+    - Include tests.
+    - Code is [rebase](http://stackoverflow.com/a/7244456/1110993).
+    - License is present.
+    - pre-commit works - without mypy error.
+    - GitHub's actions are passing.
+
+6. The taipy team will have a look at your Pull Request and will give feedback. If every requirement is valid, your
+   work will be added in the next release, congratulation!
+
+
+## Dependency management
+
+Taipy comes with multiple optional packages. You can find the list directly in the product or Taipy's packages.
+The back-end Pipfile does not install by default optional packages due to `pyodbc` requiring a driver's manual
+installation. This is not the behaviour for the front-end that installs all optional packages through its Pipfile.
+
+If you are a contributor on Taipy, be careful with dependencies, do not forget to install or uninstall depending on
+your issue.
+
+If you need to add a new dependency to Taipy, do not forget to add it in the `Pipfile` and the `setup.py`.
+Keep in mind that dependency is a vector of attack. The Taipy team limits the usage of external dependencies at the
+minimum.

+ 97 - 0
src/taipy/rest/INSTALLATION.md

@@ -0,0 +1,97 @@
+# Installation, Configuration and Run
+
+## Installation
+1. Clone the taipy rest repository
+```
+$ git clone https://github.com/Avaiga/taipy-rest
+```
+2. Enter taipy rest directory
+
+```
+$ cd taipy-rest
+```
+
+3. Install dependencies
+```
+$ pip install pipenv && pipenv install
+```
+
+## Configuration
+Before running, we need to define some variables. Taipy rest APIs depend on pre-configuration of taipy config objects,
+i.e, is mandatory to define all configuration of DataNodes, Tasks, Sequences, etc. The file containing this
+configuration needs to be passed to the application at runtime. The following variable needs to be defined:
+ - TAIPY_SETUP_FILE: the path to the file containing all of taipy object configuration
+
+If using Docker, the folder containing the file needs to be mapped as a volume for it to be accessible to the
+application.
+
+## Running
+To run the application you can either run locally with:
+```
+$ flask run
+```
+
+or it can be run inside Docker with:
+```
+$ docker-compose up
+```
+
+You can also run with a Gunicorn or wsgi server.
+
+### Running with Gunicorn
+This project provide a simple wsgi entry point to run gunicorn or uwsgi for example.
+
+For gunicorn you only need to run the following commands
+
+```
+$ pip install gunicorn
+
+$ gunicorn myapi.wsgi:app
+```
+And that's it ! Gunicorn is running on port 8000
+
+If you chose gunicorn as your wsgi server, the proper commands should be in your docker-compose file.
+
+### Running with uwsgi
+Pretty much the same as gunicorn here
+
+```
+$ pip install uwsgi
+$ uwsgi --http 127.0.0.1:5000 --module myapi.wsgi:app
+```
+
+And that's it ! Uwsgi is running on port 5000
+
+If you chose uwsgi as your wsgi server, the proper commands should be in your docker-compose file.
+
+### Deploying on Heroku
+Make sure you have a working Docker installation (e.g. docker ps) and that you’re logged in to Heroku (heroku login).
+
+Log in to Container Registry:
+
+```
+$ heroku container:login
+```
+
+Create a heroku app
+```
+$ heroku create
+```
+
+Build the image and push to Container Registry:
+```
+$ heroku container:push web
+```
+
+Then release the image:
+```
+$ heroku container:release web
+```
+
+You can now access **taipy rest** on the URL that was returned on the `heroku create` command.
+
+## Documentation
+
+All the API Documentation can be found, after running the application in the following URL:
+ - ```/redoc-ui``` ReDoc UI configured to hit OpenAPI yaml file
+ - ```/openapi.yml``` return OpenAPI specification file in yaml format

+ 21 - 0
src/taipy/rest/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Avaiga
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 1 - 0
src/taipy/rest/MANIFEST.in

@@ -0,0 +1 @@
+include src/taipy/rest/*.json

+ 28 - 0
src/taipy/rest/Pipfile

@@ -0,0 +1,28 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+apispec = {extras = ["yaml"], version = "==6.3"}
+apispec-webframeworks = "==0.5.2"
+Flask-RESTful = ">=0.3.9"
+taipy-core = {ref = "develop", git = "https://github.com/avaiga/taipy-core.git"}
+flask = "==3.0.0"
+marshmallow = "==3.20.1"
+
+[dev-packages]
+tox = ">=3.24.5"
+black = "*"
+pytest = "*"
+pre-commit = "*"
+pytest-mock = "*"
+pytest-cov = "*"
+python-dotenv = "*"
+requests = "*"
+
+[requires]
+python_version = "3.9"
+
+[pipenv]
+allow_prereleases = true

+ 88 - 0
src/taipy/rest/README.md

@@ -0,0 +1,88 @@
+# Taipy-REST
+[![Python](https://img.shields.io/pypi/pyversions/taipy-rest)](https://pypi.org/project/taipy-rest)
+[![PyPI](https://img.shields.io/pypi/v/taipy-rest.svg?label=pip&logo=PyPI&logoColor=white)](https://pypi.org/project/taipy-rest)
+
+
+## License
+Copyright 2023 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](https://www.apache.org/licenses/LICENSE-2.0.txt)
+
+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.
+
+## Usage
+  - [Taipy-REST](#taipy-rest)
+  - [License](#license)
+  - [Usage](#usage)
+  - [What is Taipy REST](#what-is-taipy-rest)
+  - [Installation](#installation)
+  - [Contributing](#contributing)
+  - [Code of conduct](#code-of-conduct)
+  - [Directory Structure](#directory-structure)
+
+
+## What is Taipy REST
+
+Taipy is a Python library for creating Business Applications. More information on our
+[website](https://www.taipy.io). Taipy is split into multiple repositories including 
+_taipy-core_ and _taipy-rest_ to let users install the minimum they need.
+
+[Taipy Core](https://github.com/Avaiga/taipy-core) mostly includes business-oriented 
+features. It helps users create and manage business applications and improve analyses 
+capability through time, conditions and hypothesis.
+
+[Taipy REST](https://github.com/Avaiga/taipy-rest) is a set of APIs built on top of the 
+_taipy-core_ library developed by Avaiga. This project is meant to be used as a complement 
+for **taipy** and its goal is to enable automation through rest APIs of processes built
+on taipy.
+
+The project comes with rest APIs that provide interaction with all of taipy modules:
+ - DataNodes
+ - Tasks
+ - Jobs
+ - Sequences
+ - Scenarios
+ - Cycles
+
+A more in depth documentation of taipy can be found [here](https://docs.taipy.io).
+
+## Installation
+
+Want to install and try _Taipy REST_? Check out our [`INSTALLATION.md`](INSTALLATION.md) file.
+
+## Contributing
+
+Want to help build _Taipy REST_? Check out our [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
+
+## Code of conduct
+
+Want to be part of the _Taipy REST_ community? Check out our 
+[`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) file.
+
+## Directory Structure
+
+- `src/taipy/rest`: Main source code folder.
+    - `api`: Endpoints and schema definitions.
+      - `resources`: Implementation of all endpoints related to taipy.
+      - `schemas`: Schemas related to taipy objects. Used for marshalling and unmarshalling data.
+      - `views`: Mapping of resources to urls
+    - `commons`: Common files shared throughout the application
+      - `templates`: Swagger and redoc templates for generating the documentation
+    - `app.py`: Flask app configuration and creation
+    - `extensions.py`: Singletons used on the application factory
+    - `rest.py`: Main python entrypoint for running _taipy-rest_ application.
+- `tests`: Unit tests.
+- `CODE_OF_CONDUCT.md`: Code of conduct for members and contributors of _taipy-rest_.
+- `CONTRIBUTING.md`: Instructions to contribute to _taipy-rest_.
+- `INSTALLATION.md`: Instructions to install _taipy-rest_.
+- `LICENSE`: The Apache 2.0 License.
+- `Pipfile`: File used by the Pipenv virtual environment to manage project dependencies.
+- `README.md`: Current file.
+- `contributors.txt`: The list of contributors
+- `setup.py`: The setup script managing building, distributing, and installing _taipy-rest_.
+- `tox.ini`: Contains test scenarios to be run.

+ 26 - 0
src/taipy/rest/__init__.py

@@ -0,0 +1,26 @@
+# Copyright 2023 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.
+
+"""# Taipy Rest
+
+The Taipy Rest package exposes the Runnable `Rest^` service to provide REST APIs on top of Taipy Core. (more details
+on Taipy Core functionalities in the [user manual](../../../manuals/core/)).
+
+Once the `Rest^` service runs, users can call REST APIs to create, read, update, submit and remove Taipy entities
+(including cycles, scenarios, sequences, tasks, jobs, and data nodes). It is handy when it comes to integrating a
+Taipy application in a more complex IT ecosystem.
+
+Please refer to [REST API](../../reference_rest/) page to get the exhaustive list of available APIs."""
+
+from ._init import *
+from .version import _get_version
+
+__version__ = _get_version()

+ 12 - 0
src/taipy/rest/_init.py

@@ -0,0 +1,12 @@
+# Copyright 2023 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.
+
+from .rest import Rest

+ 14 - 0
src/taipy/rest/api/__init__.py

@@ -0,0 +1,14 @@
+# Copyright 2023 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.
+
+from . import error_handler, views
+
+__all__ = ["views", "error_handler"]

+ 108 - 0
src/taipy/rest/api/error_handler.py

@@ -0,0 +1,108 @@
+# Copyright 2023 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.
+
+from flask import jsonify
+from marshmallow import ValidationError
+
+from taipy.core.exceptions.exceptions import (
+    NonExistingCycle,
+    NonExistingDataNode,
+    NonExistingDataNodeConfig,
+    NonExistingJob,
+    NonExistingScenario,
+    NonExistingScenarioConfig,
+    NonExistingSequence,
+    NonExistingSequenceConfig,
+    NonExistingTask,
+    NonExistingTaskConfig,
+)
+
+from .exceptions.exceptions import ConfigIdMissingException, ScenarioIdMissingException, SequenceNameMissingException
+from .views import blueprint
+
+
+def _create_404(e):
+    return {"message": e.message}, 404
+
+
+@blueprint.errorhandler(ValidationError)
+def handle_marshmallow_error(e):
+    """Return json error for marshmallow validation errors.
+
+    This will avoid having to try/catch ValidationErrors in all endpoints, returning
+    correct JSON response with associated HTTP 400 Status (https://tools.ietf.org/html/rfc7231#section-6.5.1)
+    """
+    return jsonify(e.messages), 400
+
+
+@blueprint.errorhandler(ConfigIdMissingException)
+def handle_config_id_missing_exception(e):
+    return jsonify({"message": e.message}), 400
+
+
+@blueprint.errorhandler(ScenarioIdMissingException)
+def handle_scenario_id_missing_exception(e):
+    return jsonify({"message": e.message}), 400
+
+
+@blueprint.errorhandler(SequenceNameMissingException)
+def handle_sequence_name_missing_exception(e):
+    return jsonify({"message": e.message}), 400
+
+
+@blueprint.errorhandler(NonExistingDataNode)
+def handle_data_node_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingDataNodeConfig)
+def handle_data_node_config_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingCycle)
+def handle_cycle_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingJob)
+def handle_job_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingSequence)
+def handle_sequence_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingSequenceConfig)
+def handle_sequence_config_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingScenario)
+def handle_scenario_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingScenarioConfig)
+def handle_scenario_config_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingTask)
+def handle_task_not_found(e):
+    return _create_404(e)
+
+
+@blueprint.errorhandler(NonExistingTaskConfig)
+def handle_task_config_not_found(e):
+    return _create_404(e)

+ 10 - 0
src/taipy/rest/api/exceptions/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

+ 25 - 0
src/taipy/rest/api/exceptions/exceptions.py

@@ -0,0 +1,25 @@
+# Copyright 2023 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.
+
+
+class ConfigIdMissingException(Exception):
+    def __init__(self):
+        self.message = "Config id is missing."
+
+
+class ScenarioIdMissingException(Exception):
+    def __init__(self):
+        self.message = "Scenario id is missing."
+
+
+class SequenceNameMissingException(Exception):
+    def __init__(self):
+        self.message = "Sequence name is missing."

+ 10 - 0
src/taipy/rest/api/middlewares/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

+ 34 - 0
src/taipy/rest/api/middlewares/_middleware.py

@@ -0,0 +1,34 @@
+# Copyright 2023 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.
+
+from functools import wraps
+from importlib import util
+
+from taipy.core.common._utils import _load_fct
+
+
+def _middleware(f):
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        if _using_enterprise():
+            return _enterprise_middleware()(f)(*args, **kwargs)
+        else:
+            return f(*args, **kwargs)
+
+    return wrapper
+
+
+def _using_enterprise():
+    return util.find_spec("taipy.enterprise") is not None
+
+
+def _enterprise_middleware():
+    return _load_fct("taipy.enterprise.rest.api.middlewares._middleware", "_middleware")

+ 38 - 0
src/taipy/rest/api/resources/__init__.py

@@ -0,0 +1,38 @@
+# Copyright 2023 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.
+
+from .cycle import CycleList, CycleResource
+from .datanode import DataNodeList, DataNodeReader, DataNodeResource, DataNodeWriter
+from .job import JobExecutor, JobList, JobResource
+from .scenario import ScenarioExecutor, ScenarioList, ScenarioResource
+from .sequence import SequenceExecutor, SequenceList, SequenceResource
+from .task import TaskExecutor, TaskList, TaskResource
+
+__all__ = [
+    "DataNodeResource",
+    "DataNodeList",
+    "DataNodeReader",
+    "DataNodeWriter",
+    "TaskList",
+    "TaskResource",
+    "TaskExecutor",
+    "SequenceList",
+    "SequenceResource",
+    "SequenceExecutor",
+    "ScenarioList",
+    "ScenarioResource",
+    "ScenarioExecutor",
+    "CycleResource",
+    "CycleList",
+    "JobResource",
+    "JobList",
+    "JobExecutor",
+]

+ 420 - 0
src/taipy/rest/api/resources/cycle.py

@@ -0,0 +1,420 @@
+# Copyright 2023 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.
+
+from datetime import datetime
+
+from flask import request
+from flask_restful import Resource
+
+from taipy.config.common.frequency import Frequency
+from taipy.core import Cycle
+from taipy.core.cycle._cycle_manager_factory import _CycleManagerFactory
+from taipy.core.exceptions.exceptions import NonExistingCycle
+
+from ...commons.to_from_model import _to_model
+from ..middlewares._middleware import _middleware
+from ..schemas import CycleResponseSchema, CycleSchema
+
+REPOSITORY = "cycle"
+
+
+def _get_or_raise(cycle_id: str) -> None:
+    manager = _CycleManagerFactory._build_manager()
+    cycle = manager._get(cycle_id)
+    if not cycle:
+        raise NonExistingCycle(cycle_id)
+    return cycle
+
+
+class CycleResource(Resource):
+    """Single object resource
+
+    ---
+    get:
+      tags:
+        - api
+      description: |
+        Returns a `CycleSchema^` representing the unique `Cycle^` identified by the *cycle_id*
+        given as parameter. If no cycle corresponds to *cycle_id*, a `404` error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X GET http://localhost:5000/api/v1/cycles/CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                `CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a` is the value of the *cycle_id* parameter. It
+                represents the identifier of the Cycle we want to retrieve.
+
+                In case of success here is an example of the response:
+                ``` JSON
+                {"cycle": {
+                    "frequency": "Frequency.DAILY",
+                    "creation_date": "2022-08-04T17:13:32.797384",
+                    "id": "CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a",
+                    "start_date": "2022-08-04T00:00:00",
+                    "end_date": "2022-08-04T23:59:59.999999",
+                    "name": "Frequency.DAILY_2022-08-04T17:13:32.797384"
+                ```
+
+                In case of failure here is an example of the response:
+                ``` JSON
+                {"message": "Cycle CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a not found."}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.get("http://localhost:5000/api/v1/cycles/CYCLE_223894_e019-b50b-4b9f-ac09-527a")
+                    print(response)
+                    print(response.json())
+                ```
+                `CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a` is the value of the  *cycle_id* parameter. It
+                represents the identifier of the Cycle we want to retrieve.
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                {'cycle': {
+                    'frequency': 'Frequency.DAILY',
+                    'creation_date': '2022-08-04T17:13:32.797384',
+                    'id': 'CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a',
+                    'start_date': '2022-08-04T00:00:00',
+                    'end_date': '2022-08-04T23:59:59.999999',
+                    'name': 'Frequency.DAILY_2022-08-04T17:13:32.797384'
+                ```
+
+                In case of failure here is an output example:
+                ```
+                <Response [404]>
+                {'message': 'Cycle CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a not found.'}
+
+                ```
+
+        !!! Note
+          When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_READER` role.
+
+      parameters:
+        - in: path
+          name: cycle_id
+          schema:
+            type: string
+          description: The identifier of the cycle to retrieve.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  cycle: CycleSchema
+        404:
+          description: No cycle has the *cycle_id* identifier.
+    delete:
+      tags:
+        - api
+      description: |
+        Deletes the `Cycle^` identified by the  *cycle_id* given as parameter. If the cycle does not exist,
+        a 404 error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X DELETE http://localhost:5000/api/v1/cycles/CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                `CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a` is the value of the  *cycle_id* parameter. It
+                represents the identifier of the Cycle we want to delete.
+
+                In case of success here is an example of the response:
+                ``` JSON
+                {"message": "Cycle CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a was deleted."}
+                ```
+
+                In case of failure here is an example of the response:
+                ``` JSON
+                {"message": "Cycle CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a not found."}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                response = requests.delete("http://localhost:5000/api/v1/cycles/CYCLE_794_ef21-af91-4f41-b6e8-7648eda")
+                print(response)
+                print(response.json())
+                ```
+                `CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a` is the value of the  *cycle_id* parameter. It
+                represents the identifier of the Cycle we want to delete.
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                {"message": "Cycle CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a was deleted."}
+                ```
+
+                In case of failure here is an output example:
+                ```
+                <Response [404]>
+                {'message': 'Cycle CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a not found.'}
+
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_EDITOR` role.
+
+      parameters:
+        - in: path
+          name: cycle_id
+          schema:
+            type: string
+          description: The id of the cycle to delete.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+        404:
+          description: No cycle has the  *cycle_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self, cycle_id):
+        schema = CycleResponseSchema()
+        cycle = _get_or_raise(cycle_id)
+        return {"cycle": schema.dump(_to_model(REPOSITORY, cycle))}
+
+    @_middleware
+    def delete(self, cycle_id):
+        manager = _CycleManagerFactory._build_manager()
+        _get_or_raise(cycle_id)
+        manager._delete(cycle_id)
+        return {"message": f"Cycle {cycle_id} was deleted."}
+
+
+class CycleList(Resource):
+    """Creation and get_all
+
+    ---
+    get:
+      tags:
+        - api
+      description: |
+        Returns a `CycleSchema^` list representing all existing Cycles.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X GET http://localhost:5000/api/v1/cycles
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                Here is an example of the response:
+                ``` JSON
+                [
+                    {
+                    "frequency": "Frequency.DAILY",
+                    "end_date": "2022-08-06T23:59:59.999999",
+                    "creation_date": "2022-08-06T15:45:50.223894",
+                    "start_date": "2022-08-06T00:00:00",
+                    "id": "CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a",
+                    "name": "Frequency.DAILY_2022-08-06T15:45:50.223894"
+                    }
+                ]
+                ```
+
+                If there is no cycle, the response is an empty list as follows:
+                ``` JSON
+                []
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.get("http://localhost:5000/api/v1/cycles")
+                    print(response)
+                    print(response.json())
+                ```
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                [{
+                    "frequency": "Frequency.DAILY",
+                    "end_date": "2022-08-06T23:59:59.999999",
+                    "creation_date": "2022-08-06T15:45:50.223894",
+                    "start_date": "2022-08-06T00:00:00",
+                    "id": "CYCLE_223894_e0fab919-b50b-4b9f-ac09-52f77474fa7a",
+                    "name": "Frequency.DAILY_2022-08-06T15:45:50.223894"
+                    }
+                ]
+                ```
+
+                If there is no cycle, the response is an empty list as follows:
+                ```
+                <Response [200]>
+                []
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_READER` role.
+
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                allOf:
+                  - type: object
+                    properties:
+                      results:
+                        type: array
+                        items:
+                          $ref: '#/components/schemas/CycleSchema'
+    post:
+      tags:
+        - api
+      description: |
+        Creates a new cycle from the `CycleSchema^` given in the request body.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X POST -H "Content-Type: application/json"\
+                    -d '{"frequency": "DAILY", "properties": {}, "creation_date": "2020-01-01T00:00:00",\
+                    "start_date": "2020-01-01T00:00:00", "end_date": "2020-01-01T00:00:00"}'\
+                    http://localhost:5000/api/v1/cycles
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                In the curl command line, a `CycleSchema^` is provided as JSON dictionary parameter with the curl
+                option -d (--data) to specify the various attributes of the `Cycle^` to create:
+                ``` JSON
+                {
+                    "frequency": "DAILY",
+                    "properties": {},
+                    "creation_date": "2020-01-01T00:00:00",
+                    "start_date": "2020-01-01T00:00:00",
+                    "end_date": "2020-01-01T00:00:00"
+                }
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    cycle_schema = {
+                        "frequency": "DAILY",
+                        "properties": {},
+                        "creation_date": "2020-01-01T00:00:00",
+                        "start_date": "2020-01-01T00:00:00",
+                        "end_date": "2020-01-01T00:00:00"
+                    }
+                    response = requests.post("http://localhost:5000/api/v1/cycles", json=cycle_schema)
+                    print(response)
+                    print(response.json())
+                ```
+                A `CycleSchema^` is provided as a dictionary to specify the various attributes of the `Cycle^` to
+                create.
+
+                Here is the output example:
+                ```
+                <Response [201]>
+                {
+                    'message': 'Cycle was created.',
+                    'cycle': {
+                        'frequency': 'Frequency.DAILY',
+                        'end_date': '2020-01-01T00:00:00',
+                        'creation_date': '2020-01-01T00:00:00',
+                        'start_date': '2020-01-01T00:00:00',
+                        'id': 'CYCLE_c9cc527f-a8c8-4238-8f31-42166a9817db',
+                        'name': 'Frequency.DAILY_2020-01-01T00:00:00',
+                        'properties': {}}}
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_EDITOR` role.
+
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              CycleSchema
+      responses:
+        201:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  cycle: CycleSchema
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self):
+        schema = CycleResponseSchema(many=True)
+        manager = _CycleManagerFactory._build_manager()
+        cycles = [_to_model(REPOSITORY, cycle) for cycle in manager._get_all()]
+        return schema.dump(cycles)
+
+    @_middleware
+    def post(self):
+        schema = CycleResponseSchema()
+        manager = _CycleManagerFactory._build_manager()
+
+        cycle = self.__create_cycle_from_schema(schema.load(request.json))
+        manager._set(cycle)
+
+        return {
+            "message": "Cycle was created.",
+            "cycle": schema.dump(_to_model(REPOSITORY, cycle)),
+        }, 201
+
+    def __create_cycle_from_schema(self, cycle_schema: CycleSchema):
+        return Cycle(
+            id=cycle_schema.get("id"),
+            frequency=Frequency(getattr(Frequency, cycle_schema.get("frequency", "").upper())),
+            properties=cycle_schema.get("properties", {}),
+            creation_date=datetime.fromisoformat(cycle_schema.get("creation_date")),
+            start_date=datetime.fromisoformat(cycle_schema.get("start_date")),
+            end_date=datetime.fromisoformat(cycle_schema.get("end_date")),
+        )

+ 662 - 0
src/taipy/rest/api/resources/datanode.py

@@ -0,0 +1,662 @@
+# Copyright 2023 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.
+
+from typing import List
+
+import numpy as np
+import pandas as pd
+from flask import request
+from flask_restful import Resource
+
+from taipy.config.config import Config
+from taipy.core.data._data_manager_factory import _DataManagerFactory
+from taipy.core.data.operator import Operator
+from taipy.core.exceptions.exceptions import NonExistingDataNode, NonExistingDataNodeConfig
+
+from ...commons.to_from_model import _to_model
+from ..exceptions.exceptions import ConfigIdMissingException
+from ..middlewares._middleware import _middleware
+from ..schemas import (
+    CSVDataNodeConfigSchema,
+    DataNodeFilterSchema,
+    DataNodeSchema,
+    ExcelDataNodeConfigSchema,
+    GenericDataNodeConfigSchema,
+    InMemoryDataNodeConfigSchema,
+    JSONDataNodeConfigSchema,
+    MongoCollectionDataNodeConfigSchema,
+    PickleDataNodeConfigSchema,
+    SQLDataNodeConfigSchema,
+    SQLTableDataNodeConfigSchema,
+)
+
+ds_schema_map = {
+    "csv": CSVDataNodeConfigSchema,
+    "pickle": PickleDataNodeConfigSchema,
+    "in_memory": InMemoryDataNodeConfigSchema,
+    "sql_table": SQLTableDataNodeConfigSchema,
+    "sql": SQLDataNodeConfigSchema,
+    "mongo_collection": MongoCollectionDataNodeConfigSchema,
+    "excel": ExcelDataNodeConfigSchema,
+    "generic": GenericDataNodeConfigSchema,
+    "json": JSONDataNodeConfigSchema,
+}
+
+REPOSITORY = "data"
+
+
+def _get_or_raise(data_node_id: str) -> None:
+    manager = _DataManagerFactory._build_manager()
+    data_node = manager._get(data_node_id)
+    if not data_node:
+        raise NonExistingDataNode(data_node_id)
+    return data_node
+
+
+class DataNodeResource(Resource):
+    """Single object resource
+
+    ---
+    get:
+      tags:
+        - api
+      description: |
+        Returns a `DataNodeSchema^` representing the unique `DataNode^` identified by the *datanode_id*
+        given as parameter. If no data node corresponds to *datanode_id*, a `404` error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X GET http://localhost:5000/api/v1/datanodes/DATANODE_hist_cfg_75750ed8-4e09-4e00-958d
+                    -e352ee426cc9
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                `DATANODE_hist_cfg_75750ed8-4e09-4e00-958d-e352ee426cc9` is the value of the *datanode_id* parameter. It
+                represents the identifier of the data node we want to retrieve.
+
+                In case of success here is an example of the response:
+                ``` JSON
+                {"datanode": {
+                    "id": "DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d",
+                    "config_id": "historical_data_set",
+                    "scope": "<Scope.SCENARIO: 2>",
+                    "storage_type": "csv",
+                    "name": "Name of my historical data node",
+                    "owner_id": "SCENARIO_my_awesome_scenario_97f3fd67-8556-4c62-9b3b-ef189a599a38",
+                    "last_edit_date": "2022-08-10T16:03:40.855082",
+                    "job_ids": [],
+                    "version": "latest",
+                    "cacheable": false,
+                    "validity_days": null,
+                    "validity_seconds": null,
+                    "edit_in_progress": false,
+                    "data_node_properties": {
+                        "path": "daily-min-temperatures.csv",
+                        "has_header": true}
+                    }}
+                ```
+
+                In case of failure here is an example of the response:
+                ``` JSON
+                {"message":"DataNode DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d not found"}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.get(
+                    "http://localhost:5000/api/v1/datanodes/DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d")
+                    print(response)
+                    print(response.json())
+                ```
+                `DATANODE_hist_cfg_75750ed8-4e09-4e00-958d-e352ee426cc9` is the value of the *datanode_id* parameter. It
+                represents the identifier of the data node we want to retrieve.
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                {"datanode": {
+                    "id": "DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d",
+                    "config_id": "historical_data_set",
+                    "scope": "<Scope.SCENARIO: 2>",
+                    "storage_type": "csv",
+                    "name": "Name of my historical data node",
+                    "owner_id": "SCENARIO_my_awesome_scenario_97f3fd67-8556-4c62-9b3b-ef189a599a38",
+                    "last_edit_date": "2022-08-10T16:03:40.855082",
+                    "job_ids": [],
+                    "version": "latest",
+                    "cacheable": false,
+                    "validity_days": null,
+                    "validity_seconds": null,
+                    "edit_in_progress": false,
+                    "data_node_properties": {
+                        "path": "daily-min-temperatures.csv",
+                        "has_header": true}
+                    }}
+                ```
+
+                In case of failure here is an output example:
+                ```
+                <Response [404]>
+                {"message":"DataNode DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d not found"}
+
+                ```
+
+        !!! Note
+          When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_READER` role.
+
+      parameters:
+        - in: path
+          name: datanode_id
+          schema:
+            type: string
+          description: The identifier of the data node to retrieve.
+
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  datanode: DataNodeSchema
+        404:
+          description: No data node has the *datanode_id* identifier.
+    delete:
+      tags:
+        - api
+      summary: Delete a data node.
+      description: |
+        Deletes the `DataNode^` identified by the  *datanode_id* given as parameter. If the data node does not exist,
+        a 404 error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X DELETE \
+                    http://localhost:5000/api/v1/datanodes/DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                `DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d` is the value of the
+                *datanode_id* parameter. It represents the identifier of the data node we want to delete.
+
+                In case of success here is an example of the response:
+                ``` JSON
+                {"msg": "datanode DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d deleted"}
+                ```
+
+                In case of failure here is an example of the response:
+                ``` JSON
+                {"message": "Data node DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d not found."}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.delete(
+                    "http://localhost:5000/api/v1/datanodes/DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d")
+                    print(response)
+                    print(response.json())
+                ```
+                `DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d` is the value of the
+                *datanode_id* parameter. It represents the identifier of the Cycle we want to delete.
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                {"msg": "Data node DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d deleted."}
+                ```
+
+                In case of failure here is an output example:
+                ```
+                <Response [404]>
+                {'message': 'Data node DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d not found.'}
+
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_EDITOR` role.
+
+      parameters:
+        - in: path
+          name: datanode_id
+          schema:
+            type: string
+          description: The identifier of the data node to delete.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+        404:
+          description: No data node has the *datanode_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self, datanode_id):
+        schema = DataNodeSchema()
+        datanode = _get_or_raise(datanode_id)
+        return {"datanode": schema.dump(_to_model(REPOSITORY, datanode))}
+
+    @_middleware
+    def delete(self, datanode_id):
+        _get_or_raise(datanode_id)
+        manager = _DataManagerFactory._build_manager()
+        manager._delete(datanode_id)
+        return {"message": f"Data node {datanode_id} was deleted."}
+
+
+class DataNodeList(Resource):
+    """Creation and get_all
+
+    ---
+    get:
+      tags:
+        - api
+      description: |
+        Returns a `DataNodeSchema^` list representing all existing data nodes.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X GET http://localhost:5000/api/v1/datanodes
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                Here is an example of the response:
+                ``` JSON
+                [
+                    {"datanode": {
+                        "id": "DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d",
+                        "config_id": "historical_data_set",
+                        "scope": "<Scope.SCENARIO: 2>",
+                        "storage_type": "csv",
+                        "name": "Name of my historical data node",
+                        "owner_id": "SCENARIO_my_awesome_scenario_97f3fd67-8556-4c62-9b3b-ef189a599a38",
+                        "last_edit_date": "2022-08-10T16:03:40.855082",
+                        "job_ids": [],
+                        "version": "latest",
+                        "cacheable": false,
+                        "validity_days": null,
+                        "validity_seconds": null,
+                        "edit_in_progress": false,
+                        "data_node_properties": {
+                            "path": "daily-min-temperatures.csv",
+                            "has_header": true}
+                    }}
+                ]
+                ```
+
+                If there is no data node, the response is an empty list as follows:
+                ``` JSON
+                []
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.get("http://localhost:5000/api/v1/datanodes")
+                    print(response)
+                    print(response.json())
+                ```
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                [
+                    {"datanode": {
+                        "id": "DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d",
+                        "config_id": "historical_data_set",
+                        "scope": "<Scope.SCENARIO: 2>",
+                        "storage_type": "csv",
+                        "name": "Name of my historical data node",
+                        "owner_id": "SCENARIO_my_awesome_scenario_97f3fd67-8556-4c62-9b3b-ef189a599a38",
+                        "last_edit_date": "2022-08-10T16:03:40.855082",
+                        "job_ids": [],
+                        "version": "latest",
+                        "cacheable": false,
+                        "validity_days": null,
+                        "validity_seconds": null,
+                        "edit_in_progress": false,
+                        "data_node_properties": {
+                            "path": "daily-min-temperatures.csv",
+                            "has_header": true}
+                    }}
+                ]
+                ```
+
+                If there is no data node, the response is an empty list as follows:
+                ```
+                <Response [200]>
+                []
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_READER` role.
+
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                allOf:
+                  - type: object
+                    properties:
+                      results:
+                        type: array
+                        items:
+                          $ref: '#/components/schemas/DataNodeSchema'
+    post:
+      tags:
+        - api
+      description: |
+        Creates a new data node from the *config_id* given as parameter.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X POST http://localhost:5000/api/v1/datanodes?config_id=historical_data_set
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                In this example the *config_id* value ("historical_data_set") is given as parameter directly in the
+                url. A corresponding `DataNodeConfig^` must exist and must have been configured before.
+
+                Here is the output message example:
+                ```
+                {"msg": "datanode created",
+                "datanode": {
+                    "default_path": null,
+                    "path": "daily-min-temperatures.csv",
+                    "name": null,
+                    "storage_type": "csv",
+                    "scope": 2,
+                    "has_header": true}
+                }
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.post("http://localhost:5000/api/v1/datanodes?config_id=historical_data_set")
+                    print(response)
+                    print(response.json())
+                ```
+                In this example the *config_id* value ("historical_data_set") is given as parameter directly in the
+                url. A corresponding `DataNodeConfig^` must exist and must have been configured before.
+
+                Here is the output example:
+                ```
+                <Response [201]>
+                {'msg': 'datanode created',
+                'datanode': {
+                    'name': None,
+                    'scope': 2,
+                    'path': 'daily-min-temperatures.csv',
+                    'storage_type': 'csv',
+                    'default_path': None,
+                    'has_header': True}}
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_EDITOR` role.
+
+      parameters:
+        - in: query
+          name: config_id
+          schema:
+            type: string
+          description: The identifier of the data node configuration.
+      responses:
+        201:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  datanode: DataNodeSchema
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    def fetch_config(self, config_id):
+        config = Config.data_nodes.get(config_id)
+        if not config:
+            raise NonExistingDataNodeConfig(config_id)
+        return config
+
+    @_middleware
+    def get(self):
+        schema = DataNodeSchema(many=True)
+        manager = _DataManagerFactory._build_manager()
+        datanodes = [_to_model(REPOSITORY, datanode) for datanode in manager._get_all()]
+        return schema.dump(datanodes)
+
+    @_middleware
+    def post(self):
+        args = request.args
+        config_id = args.get("config_id")
+
+        if not config_id:
+            raise ConfigIdMissingException
+
+        config = self.fetch_config(config_id)
+        schema = ds_schema_map.get(config.storage_type)()
+        manager = _DataManagerFactory._build_manager()
+        manager._bulk_get_or_create({config})
+        return {
+            "message": "Data node was created.",
+            "datanode": schema.dump(config),
+        }, 201
+
+
+class DataNodeReader(Resource):
+    """Single object resource
+
+    ---
+    get:
+      tags:
+        - api
+      description: |
+        Returns the data read from the data node identified by *datanode_id*. If the data node does not exist,
+        a 404 error is returned.
+
+        !!! Example
+
+            === "Curl"
+
+                ```shell
+                  curl -X GET \
+                  http://localhost:5000/api/v1/datanodes/DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d/read
+                ```
+
+                `DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d` is the *datanode_id*
+                parameter. It represents the identifier of the data node to read.
+
+                Here is an output example. In this case, the storage type of the data node to read is `csv`,
+                and no exposed type is specified. The data is exposed as a list of dictionaries, each dictionary
+                representing a raw of the csv file.
+                ```
+                {"data": [
+                    {"Date": "1981-01-01", "Temp": 20.7}, {"Date": "1981-01-02", "Temp": 17.9},
+                    {"Date": "1981-01-03", "Temp": 18.8}, {"Date": "1981-01-04", "Temp": 14.6},
+                    {"Date": "1981-01-05", "Temp": 15.8}, {"Date": "1981-01-06", "Temp": 15.8},
+                    {"Date": "1981-01-07", "Temp": 15.8}
+                    ]}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.get(
+                    "http://localhost:5000/api/v1/datanodes/DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d/read")
+                    print(response)
+                    print(response.json())
+                ```
+                `DATANODE_historical_data_set_9db1b542-2e45-44e7-8a85-03ef9ead173d` is the *datanode_id*
+                parameter. It represents the identifier of the data node to read.
+
+                Here is an output example. In this case, the storage type of the data node to read is `csv`,
+                and no exposed type is specified. The data is exposed as a list of dictionaries, each dictionary
+                representing a raw of the csv file.
+                ```
+                {"data": [
+                    {"Date": "1981-01-01", "Temp": 20.7}, {"Date": "1981-01-02", "Temp": 17.9},
+                    {"Date": "1981-01-03", "Temp": 18.8}, {"Date": "1981-01-04", "Temp": 14.6},
+                    {"Date": "1981-01-05", "Temp": 15.8}, {"Date": "1981-01-06", "Temp": 15.8},
+                    {"Date": "1981-01-07", "Temp": 15.8}
+                    ]}
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_READER` role.
+
+      parameters:
+        - in: path
+          name: datanode_id
+          schema:
+            type: string
+          description: The id of the data node to read.
+      requestBody:
+        content:
+          application/json:
+            schema:
+              DataNodeFilterSchema
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  data:
+                    type: Any
+                    description: The data read from the data node.
+        404:
+          description: No data node has the *datanode_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    def __make_operators(self, schema: DataNodeFilterSchema) -> List:
+        return [
+            (
+                x.get("key"),
+                x.get("value"),
+                Operator(getattr(Operator, x.get("operator", "").upper())),
+            )
+            for x in schema.get("operators")
+        ]
+
+    @_middleware
+    def get(self, datanode_id):
+        schema = DataNodeFilterSchema()
+        data = request.get_json(silent=True)
+        data_node = _get_or_raise(datanode_id)
+        operators = self.__make_operators(schema.load(data)) if data else []
+        data = data_node.filter(operators)
+        if isinstance(data, pd.DataFrame):
+            data = data.to_dict(orient="records")
+        elif isinstance(data, np.ndarray):
+            data = list(data)
+        return {"data": data}
+
+
+class DataNodeWriter(Resource):
+    """Single object resource
+
+    ---
+    put:
+      tags:
+        - api
+      summary: Write into a data node.
+      description: |
+        Write data from request body into a data node by *datanode_id*. If the data node does not exist, a 404 error is
+        returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires `TAIPY_EDITOR` role.
+
+        Code example:
+
+        ```shell
+          curl -X PUT -d '[{"path": "/abc", "type": 1}, {"path": "/def", "type": 2}]' \\
+          -H 'Content-Type: application/json' \\
+           http://localhost:5000/api/v1/datanodes/DATANODE_my_config_75750ed8-4e09-4e00-958d-e352ee426cc9/write
+        ```
+
+      parameters:
+        - in: path
+          name: datanode_id
+          schema:
+            type: string
+      requestBody:
+        content:
+          application/json:
+            schema:
+              Any
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+        404:
+          description: No data node has the *datanode_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def put(self, datanode_id):
+        data = request.json
+        data_node = _get_or_raise(datanode_id)
+        data_node.write(data)
+        return {"message": f"Data node {datanode_id} was successfully written."}

+ 287 - 0
src/taipy/rest/api/resources/job.py

@@ -0,0 +1,287 @@
+# Copyright 2023 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 uuid
+from typing import Optional
+
+from flask import request
+from flask_restful import Resource
+
+from taipy.config.config import Config
+from taipy.core import Job, JobId
+from taipy.core.exceptions.exceptions import NonExistingJob, NonExistingTaskConfig
+from taipy.core.job._job_manager_factory import _JobManagerFactory
+from taipy.core.task._task_manager_factory import _TaskManagerFactory
+
+from ..exceptions.exceptions import ConfigIdMissingException
+from ..middlewares._middleware import _middleware
+from ..schemas import JobSchema
+
+
+def _get_or_raise(job_id: str):
+    manager = _JobManagerFactory._build_manager()
+    job = manager._get(job_id)
+    if job is None:
+        raise NonExistingJob(job_id)
+    return job
+
+
+class JobResource(Resource):
+    """Single object resource
+
+    ---
+    get:
+      tags:
+        - api
+      summary: Get a job.
+      description: |
+        Return a single job by *job_id*. If the job does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), the
+          endpoint requires `TAIPY_READER` role.
+
+        Code example:
+
+        ```shell
+          curl -X GET http://localhost:5000/api/v1/jobs/JOB_my_task_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: job_id
+          schema:
+            type: string
+          description: The identifier of the job.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  job: JobSchema
+        404:
+          description: No job has the *job_id* identifier.
+    delete:
+      tags:
+        - api
+      summary: Delete a job.
+      description: |
+        Delete a job. If the job does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), the endpoint
+          requires `TAIPY_EDITOR` role.
+
+        Code example:
+
+        ```shell
+          curl -X DELETE http://localhost:5000/api/v1/jobs/JOB_my_task_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: job_id
+          schema:
+            type: string
+          description: The identifier of the job.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+        404:
+          description: No job has the *job_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self, job_id):
+        schema = JobSchema()
+        job = _get_or_raise(job_id)
+        return {"job": schema.dump(job)}
+
+    @_middleware
+    def delete(self, job_id):
+        manager = _JobManagerFactory._build_manager()
+        job = _get_or_raise(job_id)
+        manager._delete(job)
+        return {"message": f"Job {job_id} was deleted."}
+
+
+class JobList(Resource):
+    """Creation and get_all
+
+    ---
+    get:
+      tags:
+        - api
+      summary: Get all jobs.
+      description: |
+        Return an array of all jobs.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), the endpoint
+          requires `TAIPY_READER` role.
+
+        Code example:
+
+        ```shell
+          curl -X GET http://localhost:5000/api/v1/jobs
+        ```
+
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                allOf:
+                  - type: object
+                    properties:
+                      results:
+                        type: array
+                        items:
+                          $ref: '#/components/schemas/JobSchema'
+    post:
+      tags:
+        - api
+      summary: Create a job.
+      description: |
+        Create a job from a task *config_id*. If the config does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), the endpoint
+          requires `TAIPY_EDITOR` role.
+
+        Code example:
+
+        ```shell
+          curl -X POST http://localhost:5000/api/v1/jobs?task_id=TASK_my_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: query
+          name: task_id
+          schema:
+            type: string
+          description: The identifier of the task configuration.
+      responses:
+        201:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  job: JobSchema
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    def fetch_config(self, config_id):
+        config = Config.tasks.get(config_id)
+        if not config:
+            raise NonExistingTaskConfig(config_id)
+        return config
+
+    @_middleware
+    def get(self):
+        schema = JobSchema(many=True)
+        manager = _JobManagerFactory._build_manager()
+        jobs = manager._get_all()
+        return schema.dump(jobs)
+
+    @_middleware
+    def post(self):
+        args = request.args
+        task_config_id = args.get("task_id")
+
+        if not task_config_id:
+            raise ConfigIdMissingException
+
+        manager = _JobManagerFactory._build_manager()
+        schema = JobSchema()
+        job = self.__create_job_from_schema(task_config_id)
+        manager._set(job)
+        return {
+            "message": "Job was created.",
+            "job": schema.dump(job),
+        }, 201
+
+    def __create_job_from_schema(self, task_config_id: str) -> Optional[Job]:
+        task_manager = _TaskManagerFactory._build_manager()
+        task = task_manager._bulk_get_or_create([self.fetch_config(task_config_id)])[0]
+        return Job(
+            id=JobId(f"JOB_{uuid.uuid4()}"), task=task, submit_id=f"SUBMISSION_{uuid.uuid4()}", submit_entity_id=task.id
+        )
+
+
+class JobExecutor(Resource):
+    """Cancel a job
+
+    ---
+    post:
+      tags:
+        - api
+      summary: Cancel a job.
+      description: |
+        Cancel a job by *job_id*. If the job does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), the endpoint
+          requires `TAIPY_EXECUTOR` role.
+
+        Code example:
+
+        ```shell
+          curl -X POST http://localhost:5000/api/v1/jobs/cancel/JOB_my_task_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: job_id
+          schema:
+            type: string
+      responses:
+        204:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  job: JobSchema
+        404:
+          description: No job has the *job_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def post(self, job_id):
+        manager = _JobManagerFactory._build_manager()
+        job = _get_or_raise(job_id)
+        manager._cancel(job)
+        return {"message": f"Job {job_id} was cancelled."}

+ 527 - 0
src/taipy/rest/api/resources/scenario.py

@@ -0,0 +1,527 @@
+# Copyright 2023 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.
+
+from flask import request
+from flask_restful import Resource
+
+from taipy.config.config import Config
+from taipy.core.exceptions.exceptions import NonExistingScenario, NonExistingScenarioConfig
+from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
+
+from ...commons.to_from_model import _to_model
+from ..exceptions.exceptions import ConfigIdMissingException
+from ..middlewares._middleware import _middleware
+from ..schemas import ScenarioResponseSchema
+
+
+def _get_or_raise(scenario_id: str):
+    manager = _ScenarioManagerFactory._build_manager()
+    scenario = manager._get(scenario_id)
+    if scenario is None:
+        raise NonExistingScenario(scenario_id)
+    return scenario
+
+
+REPOSITORY = "scenario"
+
+
+class ScenarioResource(Resource):
+    """Single object resource
+
+    ---
+    get:
+      tags:
+        - api
+      description: |
+        Returns a `ScenarioSchema^` representing the unique scenario identified by *scenario_id*. If no scenario
+        corresponds to *scenario_id*, a `404` error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X GET http://localhost:5000/api/v1/scenarios/SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                `SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c` is the value of the *scenario_id* parameter. It
+                represents the identifier of the Scenario we want to retrieve.
+
+                In case of success here is an example of the response:
+                ``` JSON
+                {"scenario": {
+                    "cycle": "CYCLE_863418_fdd1499a-8925-4540-93fd-9dbfb4f0846d",
+                    "id": "SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c",
+                    "properties": {},
+                    "tags": [],
+                    "version": "latest",
+                    "sequences": [
+                        "SEQUENCE_mean_baseline_5af317c9-34df-48b4-8a8a-bf4007e1de99",
+                        "SEQUENCE_arima_90aef6b9-8922-4a0c-b625-b2c6f3d19fa4"],
+                    "subscribers": [],
+                    "creation_date": "2022-08-15T19:21:01.871587",
+                    "primary_scenario": true}}
+                ```
+
+                In case of failure here is an example of the response:
+                ``` JSON
+                {"message": "SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c not found."}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.get(
+                    "http://localhost:5000/api/v1/scenarios/SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c")
+                    print(response)
+                    print(response.json())
+                ```
+                `SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c` is the value of the  *scenario_id* parameter. It
+                represents the identifier of the Cycle we want to retrieve.
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                {"scenario": {
+                    "cycle": "CYCLE_863418_fdd1499a-8925-4540-93fd-9dbfb4f0846d",
+                    "id": "SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c",
+                    "properties": {},
+                    "tags": [],
+                    "version": "latest",
+                    "sequences": [
+                        "SEQUENCE_mean_baseline_5af317c9-34df-48b4-8a8a-bf4007e1de99",
+                        "SEQUENCE_arima_90aef6b9-8922-4a0c-b625-b2c6f3d19fa4"],
+                    "subscribers": [],
+                    "creation_date": "2022-08-15T19:21:01.871587",
+                    "primary_scenario": true}}
+                ```
+
+                In case of failure here is an output example:
+                ```
+                <Response [404]>
+                {'message': 'Scenario SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c not found.'}
+
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_READER` role.
+
+        parameters:
+        - in: path
+          name: scenario_id
+          schema:
+            type: string
+          description: The identifier of the scenario to retrieve.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  scenario: ScenarioSchema
+        404:
+          description: No scenario has the *scenario_id* identifier.
+    delete:
+      tags:
+        - api
+      description: |
+        Delete the `Scenario^` scenario identified by the *scenario_id* given as parameter. If the scenario does not
+        exist, a 404 error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X DELETE http://localhost:5000/api/v1/scenarios/SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                `SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c` is the value of the  *scenario_id* parameter. It
+                represents the identifier of the scenario we want to delete.
+
+                In case of success here is an example of the response:
+                ``` JSON
+                {"msg": "Scenario SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c deleted."}
+                ```
+
+                In case of failure here is an example of the response:
+                ``` JSON
+                {"message": "Scenario SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c not found."}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.delete(
+                    "http://localhost:5000/api/v1/scenarios/SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c")
+                    print(response)
+                    print(response.json())
+                ```
+                `SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c` is the value of the *scenario_id* parameter. It
+                represents the identifier of the Scenario we want to delete.
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                {"msg": "Scenario SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c deleted."}
+                ```
+
+                In case of failure here is an output example:
+                ```
+                <Response [404]>
+                {'message': 'Scenario SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c not found.'}
+
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_EDITOR` role.
+
+      parameters:
+        - in: path
+          name: scenario_id
+          schema:
+            type: string
+          description: The identifier of the scenario to delete.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+        404:
+          description: No scenario has the *scenario_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self, scenario_id):
+        schema = ScenarioResponseSchema()
+        scenario = _get_or_raise(scenario_id)
+        return {"scenario": schema.dump(_to_model(REPOSITORY, scenario))}
+
+    @_middleware
+    def delete(self, scenario_id):
+        manager = _ScenarioManagerFactory._build_manager()
+        _get_or_raise(scenario_id)
+        manager._delete(scenario_id)
+        return {"message": f"Scenario {scenario_id} was deleted."}
+
+
+class ScenarioList(Resource):
+    """Creation and get_all
+
+    ---
+    get:
+      tags:
+        - api
+      summary: Get all scenarios.
+      description: |
+        Returns a `ScenarioSchema^` list representing all existing Scenarios.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X GET http://localhost:5000/api/v1/scenarios
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                Here is an example of the response:
+                ``` JSON
+                [{
+                    "cycle": "CYCLE_863418_fdd1499a-8925-4540-93fd-9dbfb4f0846d",
+                    "id": "SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c",
+                    "properties": {},
+                    "tags": [],
+                    "version": "latest",
+                    "sequences": [
+                        "SEQUENCE_mean_baseline_5af317c9-34df-48b4-8a8a-bf4007e1de99",
+                        "SEQUENCE_arima_90aef6b9-8922-4a0c-b625-b2c6f3d19fa4"],
+                    "subscribers": [],
+                    "creation_date": "2022-08-15T19:21:01.871587",
+                    "primary_scenario": true
+                    }
+                ]
+                ```
+
+                If there is no scenario, the response is an empty list as follows:
+                ``` JSON
+                []
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.get("http://localhost:5000/api/v1/scenarios")
+                    print(response)
+                    print(response.json())
+                ```
+
+                In case of success here is an output example:
+                ```
+                <Response [200]>
+                [{
+                    "cycle": "CYCLE_863418_fdd1499a-8925-4540-93fd-9dbfb4f0846d",
+                    "id": "SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c",
+                    "properties": {},
+                    "tags": [],
+                    "version": "latest",
+                    "sequences": [
+                        "SEQUENCE_mean_baseline_5af317c9-34df-48b4-8a8a-bf4007e1de99",
+                        "SEQUENCE_arima_90aef6b9-8922-4a0c-b625-b2c6f3d19fa4"],
+                    "subscribers": [],
+                    "creation_date": "2022-08-15T19:21:01.871587",
+                    "primary_scenario": true
+                    }
+                ]
+                ```
+
+                If there is no scenario, the response is an empty list as follows:
+                ```
+                <Response [200]>
+                []
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_READER` role.
+
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                allOf:
+                  - type: object
+                    properties:
+                      results:
+                        type: array
+                        items:
+                          $ref: '#/components/schemas/ScenarioSchema'
+    post:
+      tags:
+        - api
+      description: |
+        Creates a new scenario from the  *config_id*. If the config does not exist, a 404 error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X POST http://localhost:5000/api/v1/scenarios?config_id=my_scenario_config
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                In this example the *config_id* value ("my_scenario_config") is given as parameter directly in the
+                url. A corresponding `ScenarioConfig^` must exist and must have been configured before.
+
+                Here is the output message example:
+                ```
+                {"msg": "scenario created.",
+                "scenario": {
+                    "cycle": "CYCLE_863418_fdd1499a-8925-4540-93fd-9dbfb4f0846d",
+                    "id": "SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c",
+                    "properties": {},
+                    "tags": [],
+                    "version": "latest",
+                    "sequences": [
+                        "SEQUENCE_mean_baseline_5af317c9-34df-48b4-8a8a-bf4007e1de99",
+                        "SEQUENCE_arima_90aef6b9-8922-4a0c-b625-b2c6f3d19fa4"],
+                    "subscribers": [],
+                    "creation_date": "2022-08-15T19:21:01.871587",
+                    "primary_scenario": true}
+                }
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                import requests
+                    response = requests.post("http://localhost:5000/api/v1/scenarios?config_id=my_scenario_config")
+                    print(response)
+                    print(response.json())
+                ```
+                In this example the *config_id* value ("my_scenario_config") is given as parameter directly in the
+                url. A corresponding `ScenarioConfig^` must exist and must have been configured before.
+
+                Here is the output example:
+                ```
+                <Response [201]>
+                {"msg": "scenario created.",
+                "scenario": {
+                    "cycle": "CYCLE_863418_fdd1499a-8925-4540-93fd-9dbfb4f0846d",
+                    "id": "SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c",
+                    "properties": {},
+                    "tags": [],
+                    "version": "latest",
+                    "sequences": [
+                        "SEQUENCE_mean_baseline_5af317c9-34df-48b4-8a8a-bf4007e1de99",
+                        "SEQUENCE_arima_90aef6b9-8922-4a0c-b625-b2c6f3d19fa4"],
+                    "subscribers": [],
+                    "creation_date": "2022-08-15T19:21:01.871587",
+                    "primary_scenario": true}
+                }
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_EDITOR` role.
+
+      parameters:
+        - in: query
+          name: config_id
+          schema:
+            type: string
+          description: The identifier of the scenario configuration.
+      responses:
+        201:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  scenario: ScenarioSchema
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    def fetch_config(self, config_id):
+        config = Config.scenarios.get(config_id)
+        if not config:
+            raise NonExistingScenarioConfig(config_id)
+        return config
+
+    @_middleware
+    def get(self):
+        schema = ScenarioResponseSchema(many=True)
+        manager = _ScenarioManagerFactory._build_manager()
+        scenarios = [_to_model(REPOSITORY, scenario) for scenario in manager._get_all()]
+        return schema.dump(scenarios)
+
+    @_middleware
+    def post(self):
+        args = request.args
+        config_id = args.get("config_id")
+
+        response_schema = ScenarioResponseSchema()
+        manager = _ScenarioManagerFactory._build_manager()
+
+        if not config_id:
+            raise ConfigIdMissingException
+
+        config = self.fetch_config(config_id)
+        scenario = manager._create(config)
+
+        return {
+            "message": "Scenario was created.",
+            "scenario": response_schema.dump(_to_model(REPOSITORY, scenario)),
+        }, 201
+
+
+class ScenarioExecutor(Resource):
+    """Execute a scenario
+
+    ---
+    post:
+      tags:
+        - api
+      description: |
+        Executes a scenario by *scenario_id*. If the scenario does not exist, a 404 error is returned.
+
+        !!! Example
+
+            === "Curl"
+                ```shell
+                    curl -X POST http://localhost:5000/api/v1/scenarios/submit/SCENARIO_658d-5834-4d73-84e4-a6343df5e08c
+                ```
+                In this example the REST API is served on port 5000 on localhost. We are using curl command line
+                client.
+
+                `SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c` is the value of the *scenario_id* parameter. It
+                represents the identifier of the Scenario we want to submit.
+
+                Here is the output message example:
+                ```
+                {"message": "Executed scenario SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c."}
+                ```
+
+            === "Python"
+                This Python example requires the 'requests' package to be installed (`pip install requests`).
+                ```python
+                    import requests
+                        response = requests.post(
+                        "http://localhost:5000/api/v1/scenarios/submit/SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c")
+                        print(response)
+                        print(response.json())
+                ```
+                `SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c` is the value of the *scenario_id* parameter. It
+                represents the identifier of the Scenario we want to submit.
+
+                Here is the output example:
+                ```
+                <Response [202]>
+                {"message": "Executed scenario SCENARIO_63cb358d-5834-4d73-84e4-a6343df5e08c."}
+                ```
+
+        !!! Note
+            When the authorization feature is activated (available in Taipy Enterprise edition only), this endpoint
+            requires the `TAIPY_EXECUTOR` role.
+
+      parameters:
+        - in: path
+          name: scenario_id
+          schema:
+            type: string
+          description: The identifier of the scenario to submit.
+      responses:
+        202:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  scenario: ScenarioSchema
+        404:
+          description: No scenario has the *scenario_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def post(self, scenario_id):
+        _get_or_raise(scenario_id)
+        manager = _ScenarioManagerFactory._build_manager()
+        manager._submit(scenario_id)
+        return {"message": f"Scenario {scenario_id} was submitted."}

+ 292 - 0
src/taipy/rest/api/resources/sequence.py

@@ -0,0 +1,292 @@
+# Copyright 2023 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.
+
+
+from flask import request
+from flask_restful import Resource
+
+from taipy.core.exceptions.exceptions import NonExistingScenario, NonExistingSequence
+from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
+from taipy.core.sequence._sequence_manager_factory import _SequenceManagerFactory
+
+from ...commons.to_from_model import _to_model
+from ..exceptions.exceptions import ScenarioIdMissingException, SequenceNameMissingException
+from ..middlewares._middleware import _middleware
+from ..schemas import SequenceResponseSchema
+
+
+def _get_or_raise(sequence_id: str):
+    manager = _SequenceManagerFactory._build_manager()
+    sequence = manager._get(sequence_id)
+    if sequence is None:
+        raise NonExistingSequence(sequence_id)
+    return sequence
+
+
+REPOSITORY = "sequence"
+
+
+class SequenceResource(Resource):
+    """Single object resource
+
+    ---
+    get:
+      tags:
+        - api
+      summary: Get a sequence.
+      description: |
+        Return a single sequence by sequence_id. If the sequence does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires _TAIPY_READER_ role.
+
+        Code example:
+
+        ```shell
+          curl -X GET http://localhost:5000/api/v1/sequences/SEQUENCE_my_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: sequence_id
+          schema:
+            type: string
+          description: The identifier of the sequence.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  sequence: SequenceSchema
+        404:
+          description: No sequence has the *sequence_id* identifier.
+    delete:
+      tags:
+        - api
+      summary: Delete a sequence.
+      description: |
+        Delete a sequence. If the sequence does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires _TAIPY_EDITOR_ role.
+
+        Code example:
+
+        ```shell
+          curl -X DELETE http://localhost:5000/api/v1/sequences/SEQUENCE_my_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: sequence_id
+          schema:
+            type: string
+          description: The identifier of the sequence.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+        404:
+          description: No sequence has the *sequence_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self, sequence_id):
+        schema = SequenceResponseSchema()
+        sequence = _get_or_raise(sequence_id)
+        return {"sequence": schema.dump(_to_model(REPOSITORY, sequence))}
+
+    @_middleware
+    def delete(self, sequence_id):
+        manager = _SequenceManagerFactory._build_manager()
+        _get_or_raise(sequence_id)
+        manager._delete(sequence_id)
+        return {"message": f"Sequence {sequence_id} was deleted."}
+
+
+class SequenceList(Resource):
+    """Creation and get_all
+
+    ---
+    get:
+      tags:
+        - api
+      summary: Get all sequences.
+      description: |
+        Return an array of all sequences.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires _TAIPY_READER_ role.
+
+        Code example:
+
+        ```shell
+          curl -X GET http://localhost:5000/api/v1/sequences
+        ```
+
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                allOf:
+                  - type: object
+                    properties:
+                      results:
+                        type: array
+                        items:
+                          $ref: '#/components/schemas/SequenceSchema'
+    post:
+      tags:
+        - api
+      summary: Create a sequence.
+      description: |
+        Create a sequence from scenario_id, sequence_name and task_ids. If the scenario_id does not exist or
+        sequence_name is not provided, a 404 error is returned.
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires _TAIPY_EDITOR_ role.
+
+        Code example:
+
+        ```shell
+          curl -X POST --data '{"scenario_id": "SCENARIO_scenario_id", "sequence_name": "sequence", "tasks": []}' \\
+           http://localhost:5000/api/v1/sequences
+        ```
+
+      parameters:
+        - in: query
+          name: scenario_id
+          schema:
+            type: string
+          description: The Scenario the Sequence belongs to.
+          name: sequence_name
+          schema:
+            type: string
+          description: The name of the Sequence.
+          name: tasks
+          schema:
+            type: list[string]
+          description: A list of task id of the Sequence.
+
+      responses:
+        201:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  sequence: SequenceSchema
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self):
+        schema = SequenceResponseSchema(many=True)
+        manager = _SequenceManagerFactory._build_manager()
+        sequences = [_to_model(REPOSITORY, sequence) for sequence in manager._get_all()]
+        return schema.dump(sequences)
+
+    @_middleware
+    def post(self):
+        sequence_data = request.json
+        scenario_id = sequence_data.get("scenario_id")
+        sequence_name = sequence_data.get("sequence_name")
+        sequence_task_ids = sequence_data.get("task_ids", [])
+
+        response_schema = SequenceResponseSchema()
+        if not scenario_id:
+            raise ScenarioIdMissingException
+        if not sequence_name:
+            raise SequenceNameMissingException
+
+        scenario = _ScenarioManagerFactory._build_manager()._get(scenario_id)
+        if not scenario:
+            raise NonExistingScenario(scenario_id=scenario_id)
+
+        scenario.add_sequence(sequence_name, sequence_task_ids)
+        sequence = scenario.sequences[sequence_name]
+
+        return {
+            "message": "Sequence was created.",
+            "sequence": response_schema.dump(_to_model(REPOSITORY, sequence)),
+        }, 201
+
+
+class SequenceExecutor(Resource):
+    """Execute a sequence
+
+    ---
+    post:
+      tags:
+        - api
+      summary: Execute a sequence.
+      description: |
+        Execute a sequence from sequence_id. If the sequence does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), This endpoint
+          requires _TAIPY_EXECUTOR_ role.
+
+        Code example:
+
+        ```shell
+          curl -X POST http://localhost:5000/api/v1/sequences/submit/SEQUENCE_my_config_7575-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: sequence_id
+          schema:
+            type: string
+      responses:
+        204:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  sequence: SequenceSchema
+        404:
+            description: No sequence has the *sequence_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def post(self, sequence_id):
+        _get_or_raise(sequence_id)
+        manager = _SequenceManagerFactory._build_manager()
+        manager._submit(sequence_id)
+        return {"message": f"Sequence {sequence_id} was submitted."}

+ 278 - 0
src/taipy/rest/api/resources/task.py

@@ -0,0 +1,278 @@
+# Copyright 2023 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.
+
+from flask import request
+from flask_restful import Resource
+
+from taipy.config.config import Config
+from taipy.core.exceptions.exceptions import NonExistingTask, NonExistingTaskConfig
+from taipy.core.task._task_manager_factory import _TaskManagerFactory
+
+from ...commons.to_from_model import _to_model
+from ..exceptions.exceptions import ConfigIdMissingException
+from ..middlewares._middleware import _middleware
+from ..schemas import TaskSchema
+
+
+def _get_or_raise(task_id: str):
+    manager = _TaskManagerFactory._build_manager()
+    task = manager._get(task_id)
+    if task is None:
+        raise NonExistingTask(task_id)
+    return task
+
+
+REPOSITORY = "task"
+
+
+class TaskResource(Resource):
+    """Single object resource
+
+    ---
+    get:
+      tags:
+        - api
+      summary: Get a task.
+      description: |
+        Return a single task by *task_id*. If the task does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires `TAIPY_READER` role.
+
+        Code example:
+
+        ```shell
+          curl -X GET http://localhost:5000/api/v1/tasks/TASK_my_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: task_id
+          schema:
+            type: string
+          description: The identifier of the task.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  task: TaskSchema
+        404:
+          description: No task has the *task_id* identifier.
+    delete:
+      tags:
+        - api
+      summary: Delete a task.
+      description: |
+        Delete a task. If the task does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires `TAIPY_EDITOR` role.
+
+        Code example:
+
+        ```shell
+          curl -X DELETE http://localhost:5000/api/v1/tasks/TASK_my_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+      parameters:
+        - in: path
+          name: task_id
+          schema:
+            type: string
+          description: The identifier of the task.
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+        404:
+          description: No task has the *task_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def get(self, task_id):
+        schema = TaskSchema()
+        task = _get_or_raise(task_id)
+        return {"task": schema.dump(_to_model(REPOSITORY, task))}
+
+    @_middleware
+    def delete(self, task_id):
+        manager = _TaskManagerFactory._build_manager()
+        _get_or_raise(task_id)
+        manager._delete(task_id)
+        return {"message": f"Task {task_id} was deleted."}
+
+
+class TaskList(Resource):
+    """Creation and get_all
+
+    ---
+    get:
+      tags:
+        - api
+      summary: Get all tasks.
+      description: |
+        Return an array of all tasks.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires `TAIPY_READER` role.
+
+        Code example:
+
+        ```shell
+          curl -X GET http://localhost:5000/api/v1/tasks
+        ```
+
+      responses:
+        200:
+          content:
+            application/json:
+              schema:
+                allOf:
+                  - type: object
+                    properties:
+                      results:
+                        type: array
+                        items:
+                          $ref: '#/components/schemas/TaskSchema'
+    post:
+      tags:
+        - api
+      summary: Create a task.
+      description: |
+        Create a new task from its *config_id*. If the config does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires `TAIPY_EDITOR` role.
+
+        Code example:
+
+        ```shell
+          curl -X POST http://localhost:5000/api/v1/tasks?config_id=my_task_config
+        ```
+      parameters:
+        - in: query
+          name: config_id
+          schema:
+            type: string
+          description: The identifier of the task configuration.
+      responses:
+        201:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  task: TaskSchema
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    def fetch_config(self, config_id):
+        config = Config.tasks.get(config_id)
+        if not config:
+            raise NonExistingTaskConfig(config_id)
+        return config
+
+    @_middleware
+    def get(self):
+        schema = TaskSchema(many=True)
+        manager = _TaskManagerFactory._build_manager()
+        tasks = [_to_model(REPOSITORY, task) for task in manager._get_all()]
+        return schema.dump(tasks)
+
+    @_middleware
+    def post(self):
+        args = request.args
+        config_id = args.get("config_id")
+
+        schema = TaskSchema()
+        manager = _TaskManagerFactory._build_manager()
+        if not config_id:
+            raise ConfigIdMissingException
+
+        config = self.fetch_config(config_id)
+        task = manager._bulk_get_or_create([config])[0]
+
+        return {
+            "message": "Task was created.",
+            "task": schema.dump(_to_model(REPOSITORY, task)),
+        }, 201
+
+
+class TaskExecutor(Resource):
+    """Execute a task
+
+    ---
+    post:
+      tags:
+        - api
+      summary: Execute a task.
+      description: |
+        Execute a task by *task_id*. If the task does not exist, a 404 error is returned.
+
+        !!! Note
+          When the authorization feature is activated (available in the **Enterprise** edition only), this endpoint
+          requires `TAIPY_EXECUTOR` role.
+
+        Code example:
+
+        ```shell
+          curl -X POST http://localhost:5000/api/v1/tasks/submit/TASK_my_config_75750ed8-4e09-4e00-958d-e352ee426cc9
+        ```
+
+      parameters:
+        - in: path
+          name: task_id
+          schema:
+            type: string
+      responses:
+        204:
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  message:
+                    type: string
+                    description: Status message.
+                  task: TaskSchema
+        404:
+          description: No task has the *task_id* identifier.
+    """
+
+    def __init__(self, **kwargs):
+        self.logger = kwargs.get("logger")
+
+    @_middleware
+    def post(self, task_id):
+        manager = _TaskManagerFactory._build_manager()
+        task = _get_or_raise(task_id)
+        manager._orchestrator().submit_task(task)
+        return {"message": f"Task {task_id} was submitted."}

+ 43 - 0
src/taipy/rest/api/schemas/__init__.py

@@ -0,0 +1,43 @@
+# Copyright 2023 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.
+
+from .cycle import CycleResponseSchema, CycleSchema
+from .datanode import (
+    CSVDataNodeConfigSchema,
+    DataNodeConfigSchema,
+    DataNodeFilterSchema,
+    DataNodeSchema,
+    ExcelDataNodeConfigSchema,
+    GenericDataNodeConfigSchema,
+    InMemoryDataNodeConfigSchema,
+    JSONDataNodeConfigSchema,
+    MongoCollectionDataNodeConfigSchema,
+    PickleDataNodeConfigSchema,
+    SQLDataNodeConfigSchema,
+    SQLTableDataNodeConfigSchema,
+)
+from .job import JobSchema
+from .scenario import ScenarioResponseSchema, ScenarioSchema
+from .sequence import SequenceResponseSchema, SequenceSchema
+from .task import TaskSchema
+
+__all__ = [
+    "DataNodeSchema",
+    "DataNodeFilterSchema",
+    "TaskSchema",
+    "SequenceSchema",
+    "SequenceResponseSchema",
+    "ScenarioSchema",
+    "ScenarioResponseSchema",
+    "CycleSchema",
+    "CycleResponseSchema",
+    "JobSchema",
+]

+ 25 - 0
src/taipy/rest/api/schemas/cycle.py

@@ -0,0 +1,25 @@
+# Copyright 2023 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.
+
+from marshmallow import Schema, fields
+
+
+class CycleSchema(Schema):
+    name = fields.String()
+    frequency = fields.String()
+    properties = fields.Dict()
+    creation_date = fields.String()
+    start_date = fields.String()
+    end_date = fields.String()
+
+
+class CycleResponseSchema(CycleSchema):
+    id = fields.String()

+ 101 - 0
src/taipy/rest/api/schemas/datanode.py

@@ -0,0 +1,101 @@
+# Copyright 2023 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.
+
+from marshmallow import Schema, fields, pre_dump
+
+
+class DataNodeSchema(Schema):
+    config_id = fields.String()
+    scope = fields.String()
+    id = fields.String()
+    storage_type = fields.String()
+    name = fields.String()
+    owner_id = fields.String()
+    parent_ids = fields.List(fields.String)
+    last_edit_date = fields.String()
+    job_ids = fields.List(fields.String)
+    version = fields.String()
+    cacheable = fields.Boolean()
+    validity_days = fields.Float()
+    validity_seconds = fields.Float()
+    edit_in_progress = fields.Boolean()
+    properties = fields.Dict()
+
+
+class DataNodeConfigSchema(Schema):
+    name = fields.String()
+    storage_type = fields.String()
+    scope = fields.Integer()
+    cacheable = fields.Boolean()
+
+    @pre_dump
+    def serialize_scope(self, obj, **kwargs):
+        obj.scope = obj.scope.value
+        return obj
+
+
+class CSVDataNodeConfigSchema(DataNodeConfigSchema):
+    path = fields.String()
+    default_path = fields.String()
+    has_header = fields.Boolean()
+
+
+class InMemoryDataNodeConfigSchema(DataNodeConfigSchema):
+    default_data = fields.Inferred()
+
+
+class PickleDataNodeConfigSchema(DataNodeConfigSchema):
+    path = fields.String()
+    default_path = fields.String()
+    default_data = fields.Inferred()
+
+
+class SQLTableDataNodeConfigSchema(DataNodeConfigSchema):
+    db_name = fields.String()
+    table_name = fields.String()
+
+
+class SQLDataNodeConfigSchema(DataNodeConfigSchema):
+    db_name = fields.String()
+    read_query = fields.String()
+    write_query = fields.List(fields.String())
+
+
+class MongoCollectionDataNodeConfigSchema(DataNodeConfigSchema):
+    db_name = fields.String()
+    collection_name = fields.String()
+
+
+class ExcelDataNodeConfigSchema(DataNodeConfigSchema):
+    path = fields.String()
+    default_path = fields.String()
+    has_header = fields.Boolean()
+    sheet_name = fields.String()
+
+
+class GenericDataNodeConfigSchema(DataNodeConfigSchema):
+    pass
+
+
+class JSONDataNodeConfigSchema(DataNodeConfigSchema):
+    path = fields.String()
+    default_path = fields.String()
+
+
+class OperatorSchema(Schema):
+    key = fields.String()
+    value = fields.Inferred()
+    operator = fields.String()
+
+
+class DataNodeFilterSchema(DataNodeConfigSchema):
+    operators = fields.List(fields.Nested(OperatorSchema))
+    join_operator = fields.String(default="AND")

+ 27 - 0
src/taipy/rest/api/schemas/job.py

@@ -0,0 +1,27 @@
+# Copyright 2023 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.
+
+from marshmallow import Schema, fields
+
+
+class CallableSchema(Schema):
+    fct_name = fields.String()
+    fct_module = fields.String()
+
+
+class JobSchema(Schema):
+    id = fields.String()
+    task_id = fields.String()
+    status = fields.String()
+    force = fields.Boolean()
+    creation_date = fields.String()
+    subscribers = fields.Nested(CallableSchema)
+    stacktrace = fields.List(fields.String)

+ 27 - 0
src/taipy/rest/api/schemas/scenario.py

@@ -0,0 +1,27 @@
+# Copyright 2023 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.
+
+from marshmallow import Schema, fields
+
+
+class ScenarioSchema(Schema):
+    sequences = fields.Dict()
+    properties = fields.Dict()
+    primary_scenario = fields.Boolean(default=False)
+    tags = fields.List(fields.String)
+    version = fields.String()
+
+
+class ScenarioResponseSchema(ScenarioSchema):
+    id = fields.String()
+    subscribers = fields.List(fields.Dict)
+    cycle = fields.String()
+    creation_date = fields.String()

+ 25 - 0
src/taipy/rest/api/schemas/sequence.py

@@ -0,0 +1,25 @@
+# Copyright 2023 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.
+
+from marshmallow import Schema, fields
+
+
+class SequenceSchema(Schema):
+    owner_id = fields.String()
+    parent_ids = fields.List(fields.String)
+    tasks = fields.List(fields.String)
+    version = fields.String()
+    properties = fields.Dict()
+
+
+class SequenceResponseSchema(SequenceSchema):
+    id = fields.String()
+    subscribers = fields.List(fields.Dict)

+ 24 - 0
src/taipy/rest/api/schemas/task.py

@@ -0,0 +1,24 @@
+# Copyright 2023 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.
+
+from marshmallow import Schema, fields
+
+
+class TaskSchema(Schema):
+    config_id = fields.String()
+    id = fields.String()
+    owner_id = fields.String()
+    parent_ids = fields.List(fields.String)
+    input_ids = fields.List(fields.String)
+    function_name = fields.String()
+    function_module = fields.String()
+    output_ids = fields.List(fields.String)
+    version = fields.String()

+ 213 - 0
src/taipy/rest/api/views.py

@@ -0,0 +1,213 @@
+# Copyright 2023 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.
+
+from flask import Blueprint, current_app
+from flask_restful import Api
+
+from taipy.core.common._utils import _load_fct
+from taipy.logger._taipy_logger import _TaipyLogger
+
+from ..extensions import apispec
+from .middlewares._middleware import _using_enterprise
+from .resources import (
+    CycleList,
+    CycleResource,
+    DataNodeList,
+    DataNodeReader,
+    DataNodeResource,
+    DataNodeWriter,
+    JobExecutor,
+    JobList,
+    JobResource,
+    ScenarioExecutor,
+    ScenarioList,
+    ScenarioResource,
+    SequenceExecutor,
+    SequenceList,
+    SequenceResource,
+    TaskExecutor,
+    TaskList,
+    TaskResource,
+)
+from .schemas import CycleSchema, DataNodeSchema, JobSchema, ScenarioSchema, SequenceSchema, TaskSchema
+
+_logger = _TaipyLogger._get_logger()
+
+
+blueprint = Blueprint("api", __name__, url_prefix="/api/v1")
+
+api = Api(blueprint)
+
+api.add_resource(
+    DataNodeResource,
+    "/datanodes/<string:datanode_id>/",
+    endpoint="datanode_by_id",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    DataNodeReader,
+    "/datanodes/<string:datanode_id>/read/",
+    endpoint="datanode_reader",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    DataNodeWriter,
+    "/datanodes/<string:datanode_id>/write/",
+    endpoint="datanode_writer",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    DataNodeList,
+    "/datanodes/",
+    endpoint="datanodes",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    TaskResource,
+    "/tasks/<string:task_id>/",
+    endpoint="task_by_id",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(TaskList, "/tasks/", endpoint="tasks", resource_class_kwargs={"logger": _logger})
+api.add_resource(
+    TaskExecutor,
+    "/tasks/submit/<string:task_id>/",
+    endpoint="task_submit",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    SequenceResource,
+    "/sequences/<string:sequence_id>/",
+    endpoint="sequence_by_id",
+    resource_class_kwargs={"logger": _logger},
+)
+api.add_resource(
+    SequenceList,
+    "/sequences/",
+    endpoint="sequences",
+    resource_class_kwargs={"logger": _logger},
+)
+api.add_resource(
+    SequenceExecutor,
+    "/sequences/submit/<string:sequence_id>/",
+    endpoint="sequence_submit",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    ScenarioResource,
+    "/scenarios/<string:scenario_id>/",
+    endpoint="scenario_by_id",
+    resource_class_kwargs={"logger": _logger},
+)
+api.add_resource(
+    ScenarioList,
+    "/scenarios/",
+    endpoint="scenarios",
+    resource_class_kwargs={"logger": _logger},
+)
+api.add_resource(
+    ScenarioExecutor,
+    "/scenarios/submit/<string:scenario_id>/",
+    endpoint="scenario_submit",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    CycleResource,
+    "/cycles/<string:cycle_id>/",
+    endpoint="cycle_by_id",
+    resource_class_kwargs={"logger": _logger},
+)
+api.add_resource(
+    CycleList,
+    "/cycles/",
+    endpoint="cycles",
+    resource_class_kwargs={"logger": _logger},
+)
+
+api.add_resource(
+    JobResource,
+    "/jobs/<string:job_id>/",
+    endpoint="job_by_id",
+    resource_class_kwargs={"logger": _logger},
+)
+api.add_resource(JobList, "/jobs/", endpoint="jobs", resource_class_kwargs={"logger": _logger})
+api.add_resource(
+    JobExecutor,
+    "/jobs/cancel/<string:job_id>/",
+    endpoint="job_cancel",
+    resource_class_kwargs={"logger": _logger},
+)
+
+
+def load_enterprise_resources(api: Api):
+    """
+    Load enterprise resources.
+    """
+
+    if not _using_enterprise():
+        return
+    load_resources = _load_fct("taipy.enterprise.rest.api.views", "_load_resources")
+    load_resources(api)
+
+
+load_enterprise_resources(api)
+
+
+def register_views():
+    apispec.spec.components.schema("DataNodeSchema", schema=DataNodeSchema)
+    apispec.spec.path(view=DataNodeResource, app=current_app)
+    apispec.spec.path(view=DataNodeList, app=current_app)
+    apispec.spec.path(view=DataNodeReader, app=current_app)
+    apispec.spec.path(view=DataNodeWriter, app=current_app)
+
+    apispec.spec.components.schema("TaskSchema", schema=TaskSchema)
+    apispec.spec.path(view=TaskResource, app=current_app)
+    apispec.spec.path(view=TaskList, app=current_app)
+    apispec.spec.path(view=TaskExecutor, app=current_app)
+
+    apispec.spec.components.schema("SequenceSchema", schema=SequenceSchema)
+    apispec.spec.path(view=SequenceResource, app=current_app)
+    apispec.spec.path(view=SequenceList, app=current_app)
+    apispec.spec.path(view=SequenceExecutor, app=current_app)
+
+    apispec.spec.components.schema("ScenarioSchema", schema=ScenarioSchema)
+    apispec.spec.path(view=ScenarioResource, app=current_app)
+    apispec.spec.path(view=ScenarioList, app=current_app)
+    apispec.spec.path(view=ScenarioExecutor, app=current_app)
+
+    apispec.spec.components.schema("CycleSchema", schema=CycleSchema)
+    apispec.spec.path(view=CycleResource, app=current_app)
+    apispec.spec.path(view=CycleList, app=current_app)
+
+    apispec.spec.components.schema("JobSchema", schema=JobSchema)
+    apispec.spec.path(view=JobResource, app=current_app)
+    apispec.spec.path(view=JobList, app=current_app)
+    apispec.spec.path(view=JobExecutor, app=current_app)
+
+    apispec.spec.components.schema(
+        "Any",
+        {
+            "description": "Any value",
+            "nullable": True,
+        },
+    )
+
+    if _using_enterprise():
+        _register_views = _load_fct("taipy.enterprise.rest.api.views", "_register_views")
+        _register_views(apispec)

+ 59 - 0
src/taipy/rest/app.py

@@ -0,0 +1,59 @@
+# Copyright 2023 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 os
+
+from flask import Flask
+
+from . import api
+from .commons.encoder import _CustomEncoder
+from .extensions import apispec
+
+
+def create_app(testing=False, flask_env=None, secret_key=None):
+    """Application factory, used to create application"""
+    app = Flask(__name__)
+    app.config.update(
+        ENV=os.getenv("FLASK_ENV", flask_env),
+        TESTING=os.getenv("TESTING", testing),
+        SECRET_KEY=os.getenv("SECRET_KEY", secret_key),
+    )
+    app.url_map.strict_slashes = False
+    app.config["RESTFUL_JSON"] = {"cls": _CustomEncoder}
+
+    configure_apispec(app)
+    register_blueprints(app)
+    with app.app_context():
+        api.views.register_views()
+
+    return app
+
+
+def configure_apispec(app):
+    """Configure APISpec for swagger support"""
+    apispec.init_app(app)
+
+    apispec.spec.components.schema(
+        "PaginatedResult",
+        {
+            "properties": {
+                "total": {"type": "integer"},
+                "pages": {"type": "integer"},
+                "next": {"type": "string"},
+                "prev": {"type": "string"},
+            }
+        },
+    )
+
+
+def register_blueprints(app):
+    """Register all blueprints for application"""
+    app.register_blueprint(api.views.blueprint)

+ 10 - 0
src/taipy/rest/commons/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

+ 103 - 0
src/taipy/rest/commons/apispec.py

@@ -0,0 +1,103 @@
+# Copyright 2023 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.
+
+from apispec import APISpec
+from apispec.exceptions import APISpecError
+from apispec.ext.marshmallow import MarshmallowPlugin
+from apispec_webframeworks.flask import FlaskPlugin
+from flask import Blueprint, jsonify, render_template
+
+
+class FlaskRestfulPlugin(FlaskPlugin):
+    """Small plugin override to handle flask-restful resources"""
+
+    @staticmethod
+    def _rule_for_view(view, app=None):
+        view_funcs = app.view_functions
+        endpoint = None
+
+        for ept, view_func in view_funcs.items():
+            if hasattr(view_func, "view_class"):
+                view_func = view_func.view_class
+
+            if view_func == view:
+                endpoint = ept
+
+        if not endpoint:
+            raise APISpecError("Could not find endpoint for view {0}".format(view))
+
+        # WARNING: Assume 1 rule per view function for now
+        rule = app.url_map._rules_by_endpoint[endpoint][0]
+        return rule
+
+
+class APISpecExt:
+    """Very simple and small extension to use apispec with this API as a flask extension"""
+
+    def __init__(self, app=None, **kwargs):
+        self.spec = None
+
+        if app is not None:
+            self.init_app(app, **kwargs)
+
+    def init_app(self, app, **kwargs):
+        app.config.setdefault("APISPEC_TITLE", "Taipy Rest")
+        app.config.setdefault("APISPEC_VERSION", "1.0.0")
+        app.config.setdefault("OPENAPI_VERSION", "3.0.2")
+        app.config.setdefault("SWAGGER_JSON_URL", "/swagger.json")
+        app.config.setdefault("SWAGGER_UI_URL", "/swagger-ui")
+        app.config.setdefault("OPENAPI_YAML_URL", "/openapi.yaml")
+        app.config.setdefault("REDOC_UI_URL", "/redoc-ui")
+        app.config.setdefault("SWAGGER_URL_PREFIX", None)
+
+        self.spec = APISpec(
+            title=app.config["APISPEC_TITLE"],
+            version=app.config["APISPEC_VERSION"],
+            openapi_version=app.config["OPENAPI_VERSION"],
+            plugins=[MarshmallowPlugin(), FlaskRestfulPlugin()],
+            **kwargs
+        )
+
+        blueprint = Blueprint(
+            "swagger",
+            __name__,
+            template_folder="./templates",
+            url_prefix=app.config["SWAGGER_URL_PREFIX"],
+        )
+
+        blueprint.add_url_rule(app.config["SWAGGER_JSON_URL"], "swagger_json", self.swagger_json)
+        blueprint.add_url_rule(app.config["SWAGGER_UI_URL"], "swagger_ui", self.swagger_ui)
+        blueprint.add_url_rule(app.config["OPENAPI_YAML_URL"], "openapi_yaml", self.openapi_yaml)
+        blueprint.add_url_rule(app.config["REDOC_UI_URL"], "redoc_ui", self.redoc_ui)
+
+        app.register_blueprint(blueprint)
+
+    def swagger_json(self):
+        return jsonify(self.spec.to_dict())
+
+    def swagger_ui(self):
+        return render_template("swagger.j2")
+
+    def openapi_yaml(self):
+        # Manually inject ReDoc's Authentication legend, then remove it
+        self.spec.tag(
+            {
+                "name": "authentication",
+                "x-displayName": "Authentication",
+                "description": "<SecurityDefinitions />",
+            }
+        )
+        redoc_spec = self.spec.to_yaml()
+        self.spec._tags.pop(0)
+        return redoc_spec
+
+    def redoc_ui(self):
+        return render_template("redoc.j2")

+ 28 - 0
src/taipy/rest/commons/encoder.py

@@ -0,0 +1,28 @@
+# Copyright 2023 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
+from datetime import datetime
+from enum import Enum
+from typing import Any, Union
+
+Json = Union[dict, list, str, int, float, bool, None]
+
+
+class _CustomEncoder(json.JSONEncoder):
+    def default(self, o: Any) -> Json:
+        if isinstance(o, Enum):
+            result = o.value
+        elif isinstance(o, datetime):
+            result = {"__type__": "Datetime", "__value__": o.isoformat()}
+        else:
+            result = json.JSONEncoder.default(self, o)
+        return result

+ 50 - 0
src/taipy/rest/commons/pagination.py

@@ -0,0 +1,50 @@
+# Copyright 2023 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.
+
+"""Simple helper to paginate query
+"""
+from flask import request, url_for
+
+DEFAULT_PAGE_SIZE = 50
+DEFAULT_PAGE_NUMBER = 1
+
+
+def extract_pagination(page=None, per_page=None, **request_args):
+    page = int(page) if page is not None else DEFAULT_PAGE_NUMBER
+    per_page = int(per_page) if per_page is not None else DEFAULT_PAGE_SIZE
+    return page, per_page, request_args
+
+
+def paginate(query, schema):
+    page, per_page, other_request_args = extract_pagination(**request.args)
+    page_obj = query.paginate(page=page, per_page=per_page)
+    next_ = url_for(
+        request.endpoint,
+        page=page_obj.next_num if page_obj.has_next else page_obj.page,
+        per_page=per_page,
+        **other_request_args,
+        **request.view_args
+    )
+    prev = url_for(
+        request.endpoint,
+        page=page_obj.prev_num if page_obj.has_prev else page_obj.page,
+        per_page=per_page,
+        **other_request_args,
+        **request.view_args
+    )
+
+    return {
+        "total": page_obj.total,
+        "pages": page_obj.pages,
+        "next": next_,
+        "prev": prev,
+        "results": schema.dump(page_obj.items),
+    }

+ 22 - 0
src/taipy/rest/commons/templates/redoc.j2

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Taipy Rest - ReDoc</title>
+    <!-- needed for adaptive design -->
+    <meta charset="utf-8"/>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link href="//fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
+    <!-- ReDoc doesn't change outer page styles -->
+    <style>
+      body {
+        margin: 0;
+        padding: 0;
+      }
+    </style>
+  </head>
+  <body>
+    <!-- See https://github.com/Redocly/redoc for available options-->
+    <redoc no-auto-auth required-props-first spec-url="{{ url_for('swagger.openapi_yaml') }}"></redoc>
+    <script src="//cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"></script>
+  </body>
+</html>

+ 51 - 0
src/taipy/rest/commons/templates/swagger.j2

@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf8">
+    <title>Taipy Rest - Swagger</title>
+    <link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3.28.0/swagger-ui.css">
+    <style>
+      html
+      {
+          box-sizing: border-box;
+          overflow: -moz-scrollbars-vertical;
+          overflow-y: scroll;
+      }
+      *,
+      *:before,
+      *:after
+      {
+          box-sizing: inherit;
+      }
+
+      body {
+          margin:0;
+          background: #fafafa;
+      }
+    </style>
+  </head>
+  <body>
+    <div id="swagger-ui"></div>
+    <script src="//unpkg.com/swagger-ui-dist@3.28.0/swagger-ui-bundle.js"></script>
+    <script src="//unpkg.com/swagger-ui-dist@3.28.0/swagger-ui-standalone-preset.js"></script>
+    <script type="text/javascript">
+      window.onload = function() {
+      SwaggerUIBundle({
+          deepLinking: true,
+          dom_id: '#swagger-ui',
+          showExtensions: true,
+          showCommonExtensions: true,
+          url: "{{ url_for('swagger.swagger_json') }}",
+          presets: [
+            SwaggerUIBundle.presets.apis,
+            SwaggerUIStandalonePreset
+          ],
+          plugins: [
+            SwaggerUIBundle.plugins.DownloadUrl
+          ],
+          layout: "StandaloneLayout"
+        });
+      };
+    </script>
+  </body>
+</html>

+ 28 - 0
src/taipy/rest/commons/to_from_model.py

@@ -0,0 +1,28 @@
+# Copyright 2023 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.
+
+from taipy.core.cycle._cycle_converter import _CycleConverter
+from taipy.core.data._data_converter import _DataNodeConverter
+from taipy.core.scenario._scenario_converter import _ScenarioConverter
+from taipy.core.sequence._sequence_converter import _SequenceConverter
+from taipy.core.task._task_converter import _TaskConverter
+
+entity_to_models = {
+    "scenario": _ScenarioConverter._entity_to_model,
+    "sequence": _SequenceConverter._entity_to_model,
+    "task": _TaskConverter._entity_to_model,
+    "data": _DataNodeConverter._entity_to_model,
+    "cycle": _CycleConverter._entity_to_model,
+}
+
+
+def _to_model(repository, entity, **kwargs):
+    return entity_to_models[repository](entity)

+ 8 - 0
src/taipy/rest/contributors.txt

@@ -0,0 +1,8 @@
+joaoandre-avaiga
+jrobinAV
+florian-vuillemot
+tsuu2092
+trgiangdo
+toan-quach
+dinhlongviolin1
+FabienLelaquais

+ 20 - 0
src/taipy/rest/extensions.py

@@ -0,0 +1,20 @@
+# Copyright 2023 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.
+
+"""Extensions registry
+
+All extensions here are used as singletons and
+initialized in application factory
+"""
+
+from .commons.apispec import APISpecExt
+
+apispec = APISpecExt()

+ 45 - 0
src/taipy/rest/rest.py

@@ -0,0 +1,45 @@
+# Copyright 2023 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.
+from taipy.config import Config
+
+from .app import create_app as _create_app
+
+
+class Rest:
+    """
+    Runnable Rest application serving REST APIs on top of Taipy Core functionalities.
+    """
+
+    def __init__(self):
+        """
+        Initialize a REST API server.
+
+        A Flask application is instantiated and configured using three parameters from the global
+        config.
+            - Config.global_config.testing (bool): Run the application on testing mode.
+            - Config.global_config.env (Optional[str]): The application environment.
+            - Config.global_config.secret_key (Optional[str]): Application server secret key.
+
+        However, editing these parameters is only recommended for advanced users. Indeed, the default behavior of the
+        REST server without any required configuration satisfies all the standard and basic needs.
+        """
+        self._app = _create_app(
+            Config.global_config.testing or False, Config.global_config.env, Config.global_config.secret_key
+        )
+
+    def run(self, **kwargs):
+        """
+        Start a REST API server. This method is blocking.
+
+        Parameters:
+            **kwargs : Options to provide to the application server.
+        """
+        self._app.run(**kwargs)

+ 58 - 0
src/taipy/rest/setup.py

@@ -0,0 +1,58 @@
+# Copyright 2023 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
+
+from setuptools import find_namespace_packages, find_packages, setup
+
+with open("README.md") as readme_file:
+    readme = readme_file.read()
+
+with open(f"src{os.sep}taipy{os.sep}rest{os.sep}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)}'
+    if vext := version.get("ext"):
+        version_string = f"{version_string}.{vext}"
+
+setup(
+    author="Avaiga",
+    name="taipy-rest",
+    keywords="taipy-rest",
+    python_requires=">=3.8",
+    version=version_string,
+    author_email="dev@taipy.io",
+    packages=find_namespace_packages(where="src") + find_packages(include=["taipy", "taipy.rest"]),
+    package_dir={"": "src"},
+    include_package_data=True,
+    long_description=readme,
+    long_description_content_type="text/markdown",
+    description="Library to expose taipy-core REST APIs.",
+    license="Apache License 2.0",
+    classifiers=[
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: Apache Software License",
+        "Natural Language :: English",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Programming Language :: Python :: 3.10",
+        "Programming Language :: Python :: 3.11",
+    ],
+    install_requires=[
+        "flask>=3.0.0,<3.1",
+        "flask-restful>=0.3.9,<0.4",
+        "passlib>=1.7.4,<1.8",
+        "marshmallow>=3.20.1,<3.30",
+        "apispec[yaml]>=6.3,<7.0",
+        "apispec-webframeworks>=0.5.2,<0.6",
+        "taipy-core@git+https://git@github.com/Avaiga/taipy-core.git@develop",
+    ],
+)

+ 10 - 0
src/taipy/rest/tests/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

+ 315 - 0
src/taipy/rest/tests/conftest.py

@@ -0,0 +1,315 @@
+# Copyright 2023 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 os
+import shutil
+import uuid
+from datetime import datetime, timedelta
+
+import pandas as pd
+import pytest
+from dotenv import load_dotenv
+
+from src.taipy.rest.app import create_app
+from taipy.config import Config
+from taipy.config.common.frequency import Frequency
+from taipy.config.common.scope import Scope
+from taipy.core import Cycle, DataNodeId, Job, JobId, Scenario, Sequence, Task
+from taipy.core.cycle._cycle_manager import _CycleManager
+from taipy.core.data.in_memory import InMemoryDataNode
+from taipy.core.job._job_manager import _JobManager
+from taipy.core.task._task_manager import _TaskManager
+
+from .setup.shared.algorithms import evaluate, forecast
+
+
+@pytest.fixture
+def setup_end_to_end():
+    model_cfg = Config.configure_data_node("model", path="setup/my_model.p", storage_type="pickle")
+
+    day_cfg = Config.configure_data_node(id="day")
+    forecasts_cfg = Config.configure_data_node(id="forecasts")
+    forecast_task_cfg = Config.configure_task(
+        id="forecast_task",
+        input=[model_cfg, day_cfg],
+        function=forecast,
+        output=forecasts_cfg,
+    )
+
+    historical_temperature_cfg = Config.configure_data_node(
+        "historical_temperature",
+        storage_type="csv",
+        path="setup/historical_temperature.csv",
+        has_header=True,
+    )
+    evaluation_cfg = Config.configure_data_node("evaluation")
+    evaluate_task_cfg = Config.configure_task(
+        "evaluate_task",
+        input=[historical_temperature_cfg, forecasts_cfg, day_cfg],
+        function=evaluate,
+        output=evaluation_cfg,
+    )
+
+    scenario_config = Config.configure_scenario(
+        "scenario", [forecast_task_cfg, evaluate_task_cfg], frequency=Frequency.DAILY
+    )
+    scenario_config.add_sequences({"sequence": [forecast_task_cfg, evaluate_task_cfg]})
+
+
+@pytest.fixture()
+def app():
+    load_dotenv(".testenv")
+    app = create_app(testing=True)
+    app.config.update(
+        {
+            "TESTING": True,
+        }
+    )
+    with app.app_context(), app.test_request_context():
+        yield app
+
+
+@pytest.fixture()
+def client(app):
+    return app.test_client()
+
+
+@pytest.fixture
+def datanode_data():
+    return {
+        "name": "foo",
+        "storage_type": "in_memory",
+        "scope": "scenario",
+        "default_data": ["1991-01-01T00:00:00"],
+    }
+
+
+@pytest.fixture
+def task_data():
+    return {
+        "config_id": "foo",
+        "input_ids": ["DATASOURCE_foo_3b888e17-1974-4a56-a42c-c7c96bc9cd54"],
+        "function_name": "print",
+        "function_module": "builtins",
+        "output_ids": ["DATASOURCE_foo_4d9923b8-eb9f-4f3c-8055-3a1ce8bee309"],
+    }
+
+
+@pytest.fixture
+def sequence_data():
+    return {
+        "name": "foo",
+        "task_ids": ["TASK_foo_3b888e17-1974-4a56-a42c-c7c96bc9cd54"],
+    }
+
+
+@pytest.fixture
+def scenario_data():
+    return {
+        "name": "foo",
+        "sequence_ids": ["SEQUENCE_foo_3b888e17-1974-4a56-a42c-c7c96bc9cd54"],
+        "properties": {},
+    }
+
+
+@pytest.fixture
+def default_datanode():
+    return InMemoryDataNode(
+        "input_ds",
+        Scope.SCENARIO,
+        DataNodeId("f"),
+        "my name",
+        "owner_id",
+        properties={"default_data": [1, 2, 3, 4, 5, 6]},
+    )
+
+
+@pytest.fixture
+def default_df_datanode():
+    return InMemoryDataNode(
+        "input_ds",
+        Scope.SCENARIO,
+        DataNodeId("id_uio2"),
+        "my name",
+        "owner_id",
+        properties={"default_data": pd.DataFrame([{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}])},
+    )
+
+
+@pytest.fixture
+def default_datanode_config():
+    return Config.configure_data_node(f"taipy_{uuid.uuid4().hex}", "in_memory", Scope.SCENARIO)
+
+
+@pytest.fixture
+def default_datanode_config_list():
+    configs = []
+    for i in range(10):
+        configs.append(Config.configure_data_node(id=f"ds_{i}", storage_type="in_memory", scope=Scope.SCENARIO))
+    return configs
+
+
+def __default_task():
+    input_ds = InMemoryDataNode(
+        "input_ds",
+        Scope.SCENARIO,
+        DataNodeId("id_uio"),
+        "my name",
+        "owner_id",
+        properties={"default_data": "In memory Data Source"},
+    )
+
+    output_ds = InMemoryDataNode(
+        "output_ds",
+        Scope.SCENARIO,
+        DataNodeId("id_uio"),
+        "my name",
+        "owner_id",
+        properties={"default_data": "In memory Data Source"},
+    )
+    return Task(
+        config_id="foo",
+        properties={},
+        function=print,
+        input=[input_ds],
+        output=[output_ds],
+        id=None,
+    )
+
+
+@pytest.fixture
+def default_task():
+    return __default_task()
+
+
+@pytest.fixture
+def default_task_config():
+    return Config.configure_task("task1", print, [], [])
+
+
+@pytest.fixture
+def default_task_config_list():
+    configs = []
+    for i in range(10):
+        configs.append(Config.configure_task(f"task_{i}", print, [], []))
+    return configs
+
+
+def __default_sequence():
+    return Sequence(properties={"name": "foo"}, tasks=[__default_task()], sequence_id="SEQUENCE_foo_SCENARIO_acb")
+
+
+def __task_config():
+    return Config.configure_task("task1", print, [], [])
+
+
+@pytest.fixture
+def default_sequence():
+    return __default_sequence()
+
+
+@pytest.fixture
+def default_scenario_config():
+    task_config = __task_config()
+    scenario_config = Config.configure_scenario(
+        f"taipy_{uuid.uuid4().hex}",
+        [task_config],
+    )
+    scenario_config.add_sequences({"sequence": [task_config]})
+    return scenario_config
+
+
+@pytest.fixture
+def default_scenario_config_list():
+    configs = []
+    for _ in range(10):
+        task_config = Config.configure_task(f"taipy_{uuid.uuid4().hex}", print)
+        scenario_config = Config.configure_scenario(
+            f"taipy_{uuid.uuid4().hex}",
+            [task_config],
+        )
+        scenario_config.add_sequences({"sequence": [task_config]})
+        configs.append(scenario_config)
+    return configs
+
+
+@pytest.fixture
+def default_scenario():
+    return Scenario(config_id="foo", properties={}, tasks=[__default_task()], scenario_id="SCENARIO_scenario_id")
+
+
+def __create_cycle(name="foo"):
+    now = datetime.now()
+    return Cycle(
+        name=name,
+        frequency=Frequency.DAILY,
+        properties={},
+        creation_date=now,
+        start_date=now,
+        end_date=now + timedelta(days=5),
+    )
+
+
+@pytest.fixture
+def create_cycle_list():
+    cycles = []
+    manager = _CycleManager
+    for i in range(10):
+        c = __create_cycle(f"cycle_{1}")
+        manager._set(c)
+    return cycles
+
+
+@pytest.fixture
+def cycle_data():
+    return {
+        "name": "foo",
+        "frequency": "daily",
+        "properties": {},
+        "creation_date": "2022-02-03T22:17:27.317114",
+        "start_date": "2022-02-03T22:17:27.317114",
+        "end_date": "2022-02-08T22:17:27.317114",
+    }
+
+
+@pytest.fixture
+def default_cycle():
+    return __create_cycle()
+
+
+def __create_job():
+    task_manager = _TaskManager
+    task = __default_task()
+    task_manager._set(task)
+    submit_id = f"SUBMISSION_{str(uuid.uuid4())}"
+    return Job(id=JobId(f"JOB_{uuid.uuid4()}"), task=task, submit_id=submit_id, submit_entity_id=task.id)
+
+
+@pytest.fixture
+def default_job():
+    return __create_job()
+
+
+@pytest.fixture
+def create_job_list():
+    jobs = []
+    manager = _JobManager
+    for i in range(10):
+        c = __create_job()
+        manager._set(c)
+    return jobs
+
+
+@pytest.fixture(scope="function", autouse=True)
+def cleanup_files():
+    Config.unblock_update()
+    if os.path.exists(".data"):
+        shutil.rmtree(".data")

+ 9 - 0
src/taipy/rest/tests/json/expected/cycle.json

@@ -0,0 +1,9 @@
+{
+    "id": "CYCLE_Frequency.DAILY_2022-03-31T215052.349698_4dfea10a-605f-4d02-91cd-cc8c43cd7d2f",
+    "name": "Frequency.DAILY_2022-03-31T21:50:52.349698",
+    "frequency": "<Frequency.DAILY: 1>",
+    "properties": {},
+    "creation_date": "2022-03-31T21:50:52.349698",
+    "start_date": "2022-03-31T00:00:00",
+    "end_date": "2022-03-31T23:59:59.999999"
+}

+ 18 - 0
src/taipy/rest/tests/json/expected/datanode.json

@@ -0,0 +1,18 @@
+{
+    "id": "DATANODE_day_8c0595aa-2dcf-4080-aa78-2eebd7619c9b",
+    "config_id": "day",
+    "scope": "<Scope.SCENARIO: 2>",
+    "storage_type": "pickle",
+    "name": "DATANODE_day_8c0595aa-2dcf-4080-aa78-2eebd7619c9b",
+    "owner_id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "parent_ids": ["TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70edab9189"],
+    "last_edit_date": null,
+    "job_ids": [],
+    "validity_days": null,
+    "validity_seconds": null,
+    "edit_in_progress": false,
+    "cacheable": false,
+    "data_node_properties": {
+    },
+    "version": "1.0"
+}

+ 15 - 0
src/taipy/rest/tests/json/expected/job.json

@@ -0,0 +1,15 @@
+{
+    "id": "JOB_602e112d-2bfa-4813-8d02-79da294fe56f",
+    "task_id": "TASK_evaluate_task_bf7796b3-e248-4e44-a87f-420b748ea461",
+    "status": "<Status.BLOCKED: 2>",
+    "force": false,
+    "creation_date": "2022-03-31T22:00:23.710789",
+    "subscribers": [
+        {
+            "fct_name": "_Orchestrator._on_status_change",
+            "fct_module": "taipy.core._orchestrator._orchestrator"
+        }
+    ],
+    "stacktrace": [],
+    "version": "1.0"
+}

+ 19 - 0
src/taipy/rest/tests/json/expected/scenario.json

@@ -0,0 +1,19 @@
+{
+    "id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "config_id": "scenario",
+    "tasks": [
+        "TASK_forecast_task_7e1bac33-15f1-4cfd-8702-17adeee902cb",
+        "TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70eddc6973"
+    ],
+    "additional_data_nodes": [],
+    "sequences": [
+        "SEQUENCE_sequence_0e1c5dd3-9896-4221-bfc8-6924acd11147"
+    ],
+    "properties": {},
+    "creation_date": "2022-03-31T21:50:52.360872",
+    "primary_scenario": true,
+    "subscribers": [],
+    "tags": [],
+    "cycle": "CYCLE_Frequency.DAILY_2022-03-31T215052.349698_4dfea10a-605f-4d02-91cd-cc8c43cd7d2f",
+    "version": "1.0"
+}

+ 13 - 0
src/taipy/rest/tests/json/expected/sequence.json

@@ -0,0 +1,13 @@
+{
+    "id": "SEQUENCE_sequence_0e1c5dd3-9896-4221-bfc8-6924pjg11147",
+    "owner_id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "parent_ids": [],
+    "config_id": "sequence",
+    "properties": {},
+    "tasks": [
+        "TASK_forecast_task_7e1bac33-15f1-4cfd-8702-17adeee902cb",
+        "TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70eddc6973"
+    ],
+    "subscribers": [],
+    "version": "1.0"
+}

+ 17 - 0
src/taipy/rest/tests/json/expected/task.json

@@ -0,0 +1,17 @@
+{
+    "id": "TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70eddc6973",
+    "owner_id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "parent_ids": ["SEQUENCE_sequence_0e1c5aa9-9896-4221-bfc8-6924acd11147"],
+    "config_id": "evaluate_task",
+    "input_ids": [
+        "DATANODE_historical_temperature_5525cf20-c3a3-42ce-a1c9-88e988bb49f9",
+        "DATANODE_forecasts_c1d9ace5-6099-44ce-a90f-6518db37bbf4",
+        "DATANODE_day_8c0595aa-2dcf-4080-aa78-2eebd7619c9b"
+    ],
+    "function_name": "evaluate",
+    "function_module": "tests.setup.shared.algorithms",
+    "output_ids": [
+        "DATANODE_evaluation_a208b475-11ba-452f-aec6-077e46ced49b"
+    ],
+    "version": "1.0"
+}

+ 10 - 0
src/taipy/rest/tests/setup/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

BIN
src/taipy/rest/tests/setup/my_model.p


+ 10 - 0
src/taipy/rest/tests/setup/shared/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

+ 55 - 0
src/taipy/rest/tests/setup/shared/algorithms.py

@@ -0,0 +1,55 @@
+# Copyright 2023 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 pickle
+import random
+from datetime import datetime, timedelta
+from typing import Any, Dict
+
+import pandas as pd
+
+n_predictions = 14
+
+
+def forecast(model, date: datetime):
+    dates = [date + timedelta(days=i) for i in range(n_predictions)]
+    forecasts = [f + random.uniform(0, 2) for f in model.forecast(len(dates))]
+    days = [str(dt.date()) for dt in dates]
+    res = {"Date": days, "Forecast": forecasts}
+    return pd.DataFrame.from_dict(res)
+
+
+def evaluate(cleaned: pd.DataFrame, forecasts: pd.DataFrame, date: datetime) -> Dict[str, Any]:
+    cleaned = cleaned[cleaned["Date"].isin(forecasts["Date"].tolist())]
+    forecasts_as_series = pd.Series(forecasts["Forecast"].tolist(), name="Forecast")
+    res = pd.concat([cleaned.reset_index(), forecasts_as_series], axis=1)
+    res["Delta"] = abs(res["Forecast"] - res["Value"])
+
+    return {
+        "Date": date,
+        "Dataframe": res,
+        "Mean_absolute_error": res["Delta"].mean(),
+        "Relative_error": (res["Delta"].mean() * 100) / res["Value"].mean(),
+    }
+
+
+if __name__ == "__main__":
+    model = pickle.load(open("../my_model.p", "rb"))
+    day = datetime(2020, 1, 25)
+    forecasts = forecast(model, day)
+
+    historical_temperature = pd.read_csv("../historical_temperature.csv")
+    evaluation = evaluate(historical_temperature, forecasts, day)
+
+    print(evaluation["Dataframe"])
+    print()
+    print(f'Mean absolute error : {evaluation["Mean_absolute_error"]}')
+    print(f'Relative error in %: {evaluation["Relative_error"]}')

+ 42 - 0
src/taipy/rest/tests/setup/shared/config.py

@@ -0,0 +1,42 @@
+# Copyright 2023 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.
+
+from taipy.core import Config, Frequency
+
+from .algorithms import evaluate, forecast
+
+model_cfg = Config.configure_data_node("model", path="my_model.p", storage_type="pickle")
+
+day_cfg = Config.configure_data_node(id="day")
+forecasts_cfg = Config.configure_data_node(id="forecasts")
+forecast_task_cfg = Config.configure_task(
+    id="forecast_task",
+    input=[model_cfg, day_cfg],
+    function=forecast,
+    output=forecasts_cfg,
+)
+
+historical_temperature_cfg = Config.configure_data_node(
+    "historical_temperature",
+    storage_type="csv",
+    path="historical_temperature.csv",
+    has_header=True,
+)
+evaluation_cfg = Config.configure_data_node("evaluation")
+evaluate_task_cfg = Config.configure_task(
+    "evaluate_task",
+    input=[historical_temperature_cfg, forecasts_cfg, day_cfg],
+    function=evaluate,
+    output=evaluation_cfg,
+)
+
+scenario_cfg = Config.configure_scenario("scenario", [forecast_task_cfg, evaluate_task_cfg], frequency=Frequency.DAILY)
+scenario_cfg.add_sequences({"sequence": [forecast_task_cfg, evaluate_task_cfg]})

+ 62 - 0
src/taipy/rest/tests/test_cycle.py

@@ -0,0 +1,62 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+from flask import url_for
+
+
+def test_get_cycle(client, default_cycle):
+    # test 404
+    cycle_url = url_for("api.cycle_by_id", cycle_id="foo")
+    rep = client.get(cycle_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.cycle._cycle_manager._CycleManager._get") as manager_mock:
+        manager_mock.return_value = default_cycle
+
+        # test get_cycle
+        rep = client.get(url_for("api.cycle_by_id", cycle_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_cycle(client):
+    # test 404
+    cycle_url = url_for("api.cycle_by_id", cycle_id="foo")
+    rep = client.get(cycle_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.cycle._cycle_manager._CycleManager._delete"), mock.patch(
+        "taipy.core.cycle._cycle_manager._CycleManager._get"
+    ):
+        # test get_cycle
+        rep = client.delete(url_for("api.cycle_by_id", cycle_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_cycle(client, cycle_data):
+    # without config param
+    cycles_url = url_for("api.cycles")
+    data = {"bad": "data"}
+    rep = client.post(cycles_url, json=data)
+    assert rep.status_code == 400
+
+    rep = client.post(cycles_url, json=cycle_data)
+    assert rep.status_code == 201
+
+
+def test_get_all_cycles(client, create_cycle_list):
+    cycles_url = url_for("api.cycles")
+    rep = client.get(cycles_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10

+ 111 - 0
src/taipy/rest/tests/test_datanode.py

@@ -0,0 +1,111 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+import pytest
+from flask import url_for
+
+
+def test_get_datanode(client, default_datanode):
+    # test 404
+    user_url = url_for("api.datanode_by_id", datanode_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.data._data_manager._DataManager._get") as manager_mock:
+        manager_mock.return_value = default_datanode
+        # test get_datanode
+        rep = client.get(url_for("api.datanode_by_id", datanode_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_datanode(client):
+    # test 404
+    user_url = url_for("api.datanode_by_id", datanode_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.data._data_manager._DataManager._delete"), mock.patch(
+        "taipy.core.data._data_manager._DataManager._get"
+    ):
+        # test get_datanode
+        rep = client.delete(url_for("api.datanode_by_id", datanode_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_datanode(client, default_datanode_config):
+    # without config param
+    datanodes_url = url_for("api.datanodes")
+    rep = client.post(datanodes_url)
+    assert rep.status_code == 400
+
+    # config does not exist
+    datanodes_url = url_for("api.datanodes", config_id="foo")
+    rep = client.post(datanodes_url)
+    assert rep.status_code == 404
+
+    with mock.patch("src.taipy.rest.api.resources.datanode.DataNodeList.fetch_config") as config_mock:
+        config_mock.return_value = default_datanode_config
+        datanodes_url = url_for("api.datanodes", config_id="bar")
+        rep = client.post(datanodes_url)
+        assert rep.status_code == 201
+
+
+def test_get_all_datanodes(client, default_datanode_config_list):
+    for ds in range(10):
+        with mock.patch("src.taipy.rest.api.resources.datanode.DataNodeList.fetch_config") as config_mock:
+            config_mock.return_value = default_datanode_config_list[ds]
+            datanodes_url = url_for("api.datanodes", config_id=config_mock.name)
+            client.post(datanodes_url)
+
+    rep = client.get(datanodes_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10
+
+
+def test_read_datanode(client, default_df_datanode):
+    with mock.patch("taipy.core.data._data_manager._DataManager._get") as config_mock:
+        config_mock.return_value = default_df_datanode
+
+        # without operators
+        datanodes_url = url_for("api.datanode_reader", datanode_id="foo")
+        rep = client.get(datanodes_url, json={})
+        assert rep.status_code == 200
+
+        # Without operators and body
+        rep = client.get(datanodes_url)
+        assert rep.status_code == 200
+
+        # TODO: Revisit filter test
+        # operators = {"operators": [{"key": "a", "value": 5, "operator": "LESS_THAN"}]}
+        # rep = client.get(datanodes_url, json=operators)
+        # assert rep.status_code == 200
+
+
+def test_write_datanode(client, default_datanode):
+    with mock.patch("taipy.core.data._data_manager._DataManager._get") as config_mock:
+        config_mock.return_value = default_datanode
+        # Get DataNode
+        datanodes_read_url = url_for("api.datanode_reader", datanode_id=default_datanode.id)
+        rep = client.get(datanodes_read_url, json={})
+        assert rep.status_code == 200
+        assert rep.json == {"data": [1, 2, 3, 4, 5, 6]}
+
+        datanodes_write_url = url_for("api.datanode_writer", datanode_id=default_datanode.id)
+        rep = client.put(datanodes_write_url, json=[1, 2, 3])
+        assert rep.status_code == 200
+
+        rep = client.get(datanodes_read_url, json={})
+        assert rep.status_code == 200
+        assert rep.json == {"data": [1, 2, 3]}

+ 100 - 0
src/taipy/rest/tests/test_end_to_end.py

@@ -0,0 +1,100 @@
+# Copyright 2023 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
+from typing import Dict
+
+from flask import url_for
+
+
+def create_and_submit_scenario(config_id: str, client) -> Dict:
+    response = client.post(url_for("api.scenarios", config_id=config_id))
+    assert response.status_code == 201
+
+    scenario = response.json.get("scenario")
+    assert (set(scenario) - set(json.load(open("tests/json/expected/scenario.json")))) == set()
+
+    response = client.post(url_for("api.scenario_submit", scenario_id=scenario.get("id")))
+    assert response.status_code == 200
+
+    return scenario
+
+
+def get(url, name, client) -> Dict:
+    response = client.get(url)
+    returned_data = response.json.get(name)
+
+    assert (set(returned_data) - set(json.load(open(f"tests/json/expected/{name}.json")))) == set()
+
+    return returned_data
+
+
+def get_assert_status(url, client, status_code) -> None:
+    response = client.get(url)
+    assert response.status_code == status_code
+
+
+def get_all(url, expected_quantity, client):
+    response = client.get(url)
+
+    assert len(response.json) == expected_quantity
+
+
+def delete(url, client):
+    response = client.delete(url)
+
+    assert response.status_code == 200
+
+
+def test_end_to_end(client, setup_end_to_end):
+    # Create Scenario: Should also create all of its dependencies(sequences, tasks, datanodes, etc)
+    scenario = create_and_submit_scenario("scenario", client)
+
+    # Get other models and verify if they return the necessary fields
+    cycle = get(url_for("api.cycle_by_id", cycle_id=scenario.get("cycle")), "cycle", client)
+    sequence = get(
+        url_for("api.sequence_by_id", sequence_id=f"SEQUENCE_sequence_{scenario['id']}"),
+        "sequence",
+        client,
+    )
+    task = get(url_for("api.task_by_id", task_id=sequence.get("tasks")[0]), "task", client)
+    datanode = get(
+        url_for("api.datanode_by_id", datanode_id=task.get("input_ids")[0]),
+        "datanode",
+        client,
+    )
+    # Get All
+    get_all(url_for("api.scenarios"), 1, client)
+    get_all(url_for("api.cycles"), 1, client)
+    get_all(url_for("api.sequences"), 1, client)
+    get_all(url_for("api.tasks"), 2, client)
+    get_all(url_for("api.datanodes"), 5, client)
+    get_all(url_for("api.jobs"), 2, client)
+
+    # Delete entities
+    delete(url_for("api.cycle_by_id", cycle_id=cycle.get("id")), client)
+    delete(url_for("api.sequence_by_id", sequence_id=sequence.get("id")), client)
+    delete(url_for("api.task_by_id", task_id=task.get("id")), client)
+    delete(url_for("api.datanode_by_id", datanode_id=datanode.get("id")), client)
+
+    # Check status code
+    # Non-existing entities should return 404
+    get_assert_status(url_for("api.cycle_by_id", cycle_id=9999999), client, 404)
+    get_assert_status(url_for("api.scenario_by_id", scenario_id=9999999), client, 404)
+    get_assert_status(url_for("api.sequence_by_id", sequence_id=9999999), client, 404)
+    get_assert_status(url_for("api.task_by_id", task_id=9999999), client, 404)
+    get_assert_status(url_for("api.datanode_by_id", datanode_id=9999999), client, 404)
+
+    # Check URL with and without trailing slashes
+    url_with_slash = url_for("api.scenarios")
+    url_without_slash = url_for("api.scenarios")[:-1]
+    get_all(url_with_slash, 1, client)
+    get_all(url_without_slash, 1, client)

+ 83 - 0
src/taipy/rest/tests/test_job.py

@@ -0,0 +1,83 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+from flask import url_for
+
+
+def test_get_job(client, default_job):
+    # test 404
+    user_url = url_for("api.job_by_id", job_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.job._job_manager._JobManager._get") as manager_mock:
+        manager_mock.return_value = default_job
+
+        # test get_job
+        rep = client.get(url_for("api.job_by_id", job_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_job(client):
+    # test 404
+    user_url = url_for("api.job_by_id", job_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.job._job_manager._JobManager._delete"), mock.patch(
+        "taipy.core.job._job_manager._JobManager._get"
+    ):
+        # test get_job
+        rep = client.delete(url_for("api.job_by_id", job_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_job(client, default_task_config):
+    # without config param
+    jobs_url = url_for("api.jobs")
+    rep = client.post(jobs_url)
+    assert rep.status_code == 400
+
+    with mock.patch("src.taipy.rest.api.resources.job.JobList.fetch_config") as config_mock:
+        config_mock.return_value = default_task_config
+        jobs_url = url_for("api.jobs", task_id="foo")
+        rep = client.post(jobs_url)
+        assert rep.status_code == 201
+
+
+def test_get_all_jobs(client, create_job_list):
+    jobs_url = url_for("api.jobs")
+    rep = client.get(jobs_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10
+
+
+def test_cancel_job(client, default_job):
+    # test 404
+    from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
+
+    _OrchestratorFactory._build_orchestrator()
+    _OrchestratorFactory._build_dispatcher()
+
+    user_url = url_for("api.job_cancel", job_id="foo")
+    rep = client.post(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.job._job_manager._JobManager._get") as manager_mock:
+        manager_mock.return_value = default_job
+
+        # test get_job
+        rep = client.post(url_for("api.job_cancel", job_id="foo"))
+        assert rep.status_code == 200

+ 59 - 0
src/taipy/rest/tests/test_middleware.py

@@ -0,0 +1,59 @@
+# Copyright 2023 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.
+
+from functools import wraps
+from unittest.mock import MagicMock, patch
+
+from src.taipy.rest.api.middlewares._middleware import _middleware
+
+
+def mock_enterprise_middleware(f):
+    @wraps(f)
+    def wrapper(*args, **kwargs):
+        return f(*args, **kwargs)
+
+    return wrapper
+
+
+@patch("src.taipy.rest.api.middlewares._middleware._using_enterprise")
+@patch("src.taipy.rest.api.middlewares._middleware._enterprise_middleware")
+def test_enterprise_middleware_applied_when_enterprise_is_installed(
+    enterprise_middleware: MagicMock, using_enterprise: MagicMock
+):
+    enterprise_middleware.return_value = mock_enterprise_middleware
+    using_enterprise.return_value = True
+
+    @_middleware
+    def f():
+        return "f"
+
+    rv = f()
+    assert rv == "f"
+    using_enterprise.assert_called_once()
+    enterprise_middleware.assert_called_once()
+
+
+@patch("src.taipy.rest.api.middlewares._middleware._using_enterprise")
+@patch("src.taipy.rest.api.middlewares._middleware._enterprise_middleware")
+def test_enterprise_middleware_not_applied_when_enterprise_is_not_installed(
+    enterprise_middleware: MagicMock, using_enterprise: MagicMock
+):
+    enterprise_middleware.return_value = mock_enterprise_middleware
+    using_enterprise.return_value = False
+
+    @_middleware
+    def f():
+        return "f"
+
+    rv = f()
+    assert rv == "f"
+    using_enterprise.assert_called_once()
+    enterprise_middleware.assert_not_called()

+ 90 - 0
src/taipy/rest/tests/test_scenario.py

@@ -0,0 +1,90 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+import pytest
+from flask import url_for
+
+
+def test_get_scenario(client, default_scenario):
+    # test 404
+    user_url = url_for("api.scenario_by_id", scenario_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get") as manager_mock:
+        manager_mock.return_value = default_scenario
+
+        # test get_scenario
+        rep = client.get(url_for("api.scenario_by_id", scenario_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_scenario(client):
+    # test 404
+    user_url = url_for("api.scenario_by_id", scenario_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._delete"), mock.patch(
+        "taipy.core.scenario._scenario_manager._ScenarioManager._get"
+    ):
+        # test get_scenario
+        rep = client.delete(url_for("api.scenario_by_id", scenario_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_scenario(client, default_scenario_config):
+    # without config param
+    scenarios_url = url_for("api.scenarios")
+    rep = client.post(scenarios_url)
+    assert rep.status_code == 400
+
+    # config does not exist
+    scenarios_url = url_for("api.scenarios", config_id="foo")
+    rep = client.post(scenarios_url)
+    assert rep.status_code == 404
+
+    with mock.patch("src.taipy.rest.api.resources.scenario.ScenarioList.fetch_config") as config_mock:
+        config_mock.return_value = default_scenario_config
+        scenarios_url = url_for("api.scenarios", config_id="bar")
+        rep = client.post(scenarios_url)
+        assert rep.status_code == 201
+
+
+def test_get_all_scenarios(client, default_sequence, default_scenario_config_list):
+    for ds in range(10):
+        with mock.patch("src.taipy.rest.api.resources.scenario.ScenarioList.fetch_config") as config_mock:
+            config_mock.return_value = default_scenario_config_list[ds]
+            scenarios_url = url_for("api.scenarios", config_id=config_mock.name)
+            client.post(scenarios_url)
+
+    rep = client.get(scenarios_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10
+
+
+@pytest.mark.xfail()
+def test_execute_scenario(client, default_scenario):
+    # test 404
+    user_url = url_for("api.scenario_submit", scenario_id="foo")
+    rep = client.post(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get") as manager_mock:
+        manager_mock.return_value = default_scenario
+
+        # test get_scenario
+        rep = client.post(url_for("api.scenario_submit", scenario_id="foo"))
+        assert rep.status_code == 200

+ 102 - 0
src/taipy/rest/tests/test_sequence.py

@@ -0,0 +1,102 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+import pytest
+from flask import url_for
+
+from src.taipy.rest.api.exceptions.exceptions import ScenarioIdMissingException, SequenceNameMissingException
+from taipy.core.exceptions.exceptions import NonExistingScenario
+from taipy.core.scenario._scenario_manager_factory import _ScenarioManagerFactory
+
+
+def test_get_sequence(client, default_sequence):
+    # test 404
+    user_url = url_for("api.sequence_by_id", sequence_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.sequence._sequence_manager._SequenceManager._get") as manager_mock:
+        manager_mock.return_value = default_sequence
+
+        # test get_sequence
+        rep = client.get(url_for("api.sequence_by_id", sequence_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_sequence(client):
+    # test 404
+    user_url = url_for("api.sequence_by_id", sequence_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.sequence._sequence_manager._SequenceManager._delete"), mock.patch(
+        "taipy.core.sequence._sequence_manager._SequenceManager._get"
+    ):
+        # test get_sequence
+        rep = client.delete(url_for("api.sequence_by_id", sequence_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_sequence(client, default_scenario):
+    sequences_url = url_for("api.sequences")
+    rep = client.post(sequences_url, json={})
+    assert rep.status_code == 400
+    assert rep.json == {"message": "Scenario id is missing."}
+
+    sequences_url = url_for("api.sequences")
+    rep = client.post(sequences_url, json={"scenario_id": "SCENARIO_scenario_id"})
+    assert rep.status_code == 400
+    assert rep.json == {"message": "Sequence name is missing."}
+
+    sequences_url = url_for("api.sequences")
+    rep = client.post(sequences_url, json={"scenario_id": "SCENARIO_scenario_id", "sequence_name": "sequence"})
+    assert rep.status_code == 404
+
+    _ScenarioManagerFactory._build_manager()._set(default_scenario)
+    with mock.patch("taipy.core.scenario._scenario_manager._ScenarioManager._get") as config_mock:
+        config_mock.return_value = default_scenario
+        sequences_url = url_for("api.sequences")
+        rep = client.post(
+            sequences_url, json={"scenario_id": default_scenario.id, "sequence_name": "sequence", "tasks": []}
+        )
+        assert rep.status_code == 201
+
+
+def test_get_all_sequences(client, default_scenario_config_list):
+    for ds in range(10):
+        with mock.patch("src.taipy.rest.api.resources.scenario.ScenarioList.fetch_config") as config_mock:
+            config_mock.return_value = default_scenario_config_list[ds]
+            scenario_url = url_for("api.scenarios", config_id=config_mock.name)
+            client.post(scenario_url)
+
+    sequences_url = url_for("api.sequences")
+    rep = client.get(sequences_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10
+
+
+@pytest.mark.xfail()
+def test_execute_sequence(client, default_sequence):
+    # test 404
+    user_url = url_for("api.sequence_submit", sequence_id="foo")
+    rep = client.post(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.sequence._sequence_manager._SequenceManager._get") as manager_mock:
+        manager_mock.return_value = default_sequence
+
+        # test get_sequence
+        rep = client.post(url_for("api.sequence_submit", sequence_id="foo"))
+        assert rep.status_code == 200

+ 88 - 0
src/taipy/rest/tests/test_task.py

@@ -0,0 +1,88 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+from flask import url_for
+
+
+def test_get_task(client, default_task):
+    # test 404
+    user_url = url_for("api.task_by_id", task_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.task._task_manager._TaskManager._get") as manager_mock:
+        manager_mock.return_value = default_task
+
+        # test get_task
+        rep = client.get(url_for("api.task_by_id", task_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_task(client):
+    # test 404
+    user_url = url_for("api.task_by_id", task_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.task._task_manager._TaskManager._delete"), mock.patch(
+        "taipy.core.task._task_manager._TaskManager._get"
+    ):
+        # test get_task
+        rep = client.delete(url_for("api.task_by_id", task_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_task(client, default_task_config):
+    # without config param
+    tasks_url = url_for("api.tasks")
+    rep = client.post(tasks_url)
+    assert rep.status_code == 400
+
+    # config does not exist
+    tasks_url = url_for("api.tasks", config_id="foo")
+    rep = client.post(tasks_url)
+    assert rep.status_code == 404
+
+    with mock.patch("src.taipy.rest.api.resources.task.TaskList.fetch_config") as config_mock:
+        config_mock.return_value = default_task_config
+        tasks_url = url_for("api.tasks", config_id="bar")
+        rep = client.post(tasks_url)
+        assert rep.status_code == 201
+
+
+def test_get_all_tasks(client, task_data, default_task_config_list):
+    for ds in range(10):
+        with mock.patch("src.taipy.rest.api.resources.task.TaskList.fetch_config") as config_mock:
+            config_mock.return_value = default_task_config_list[ds]
+            tasks_url = url_for("api.tasks", config_id=config_mock.name)
+            client.post(tasks_url)
+
+    rep = client.get(tasks_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10
+
+
+def test_execute_task(client, default_task):
+    # test 404
+    user_url = url_for("api.task_submit", task_id="foo")
+    rep = client.post(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.task._task_manager._TaskManager._get") as manager_mock:
+        manager_mock.return_value = default_task
+
+        # test get_task
+        rep = client.post(url_for("api.task_submit", task_id="foo"))
+        assert rep.status_code == 200

+ 44 - 0
src/taipy/rest/tox.ini

@@ -0,0 +1,44 @@
+[tox]
+skipsdist = true
+isolated_build = true
+envlist = lint, coverage
+
+[pytest]
+filterwarnings =
+    ignore::DeprecationWarning
+
+[testenv]
+allowlist_externals = pytest
+deps = pipenv==2023.7.23
+
+[testenv:lint]
+platform = linux
+allowlist_externals =
+    isort
+    black
+    flake8
+deps =
+    isort
+    black
+    flake8
+commands =
+    isort src
+    black src tests
+    flake8 src tests
+
+[testenv:tests]
+commands =
+    pipenv install --dev
+    pytest tests
+
+[testenv:coverage]
+platform = linux
+deps =
+    pipenv==2023.7.23
+    coverage
+commands =
+    coverage erase
+    pipenv install --dev
+    pytest -s --cov=src --cov-append --cov-report=xml --cov-report term-missing tests
+    coverage report
+    coverage html

+ 1 - 0
src/taipy/rest/version.json

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

+ 22 - 0
src/taipy/rest/version.py

@@ -0,0 +1,22 @@
+# Copyright 2023 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
+
+
+def _get_version():
+    with open(f"{os.path.dirname(os.path.abspath(__file__))}{os.sep}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)}'
+        if vext := version.get("ext"):
+            version_string = f"{version_string}.{vext}"
+    return version_string

+ 10 - 0
tests/rest/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

+ 315 - 0
tests/rest/conftest.py

@@ -0,0 +1,315 @@
+# Copyright 2023 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 os
+import shutil
+import uuid
+from datetime import datetime, timedelta
+
+import pandas as pd
+import pytest
+from dotenv import load_dotenv
+
+from src.taipy.rest.app import create_app
+from taipy.config import Config
+from taipy.config.common.frequency import Frequency
+from taipy.config.common.scope import Scope
+from taipy.core import Cycle, DataNodeId, Job, JobId, Scenario, Sequence, Task
+from taipy.core.cycle._cycle_manager import _CycleManager
+from taipy.core.data.in_memory import InMemoryDataNode
+from taipy.core.job._job_manager import _JobManager
+from taipy.core.task._task_manager import _TaskManager
+
+from .setup.shared.algorithms import evaluate, forecast
+
+
+@pytest.fixture
+def setup_end_to_end():
+    model_cfg = Config.configure_data_node("model", path="setup/my_model.p", storage_type="pickle")
+
+    day_cfg = Config.configure_data_node(id="day")
+    forecasts_cfg = Config.configure_data_node(id="forecasts")
+    forecast_task_cfg = Config.configure_task(
+        id="forecast_task",
+        input=[model_cfg, day_cfg],
+        function=forecast,
+        output=forecasts_cfg,
+    )
+
+    historical_temperature_cfg = Config.configure_data_node(
+        "historical_temperature",
+        storage_type="csv",
+        path="setup/historical_temperature.csv",
+        has_header=True,
+    )
+    evaluation_cfg = Config.configure_data_node("evaluation")
+    evaluate_task_cfg = Config.configure_task(
+        "evaluate_task",
+        input=[historical_temperature_cfg, forecasts_cfg, day_cfg],
+        function=evaluate,
+        output=evaluation_cfg,
+    )
+
+    scenario_config = Config.configure_scenario(
+        "scenario", [forecast_task_cfg, evaluate_task_cfg], frequency=Frequency.DAILY
+    )
+    scenario_config.add_sequences({"sequence": [forecast_task_cfg, evaluate_task_cfg]})
+
+
+@pytest.fixture()
+def app():
+    load_dotenv(".testenv")
+    app = create_app(testing=True)
+    app.config.update(
+        {
+            "TESTING": True,
+        }
+    )
+    with app.app_context(), app.test_request_context():
+        yield app
+
+
+@pytest.fixture()
+def client(app):
+    return app.test_client()
+
+
+@pytest.fixture
+def datanode_data():
+    return {
+        "name": "foo",
+        "storage_type": "in_memory",
+        "scope": "scenario",
+        "default_data": ["1991-01-01T00:00:00"],
+    }
+
+
+@pytest.fixture
+def task_data():
+    return {
+        "config_id": "foo",
+        "input_ids": ["DATASOURCE_foo_3b888e17-1974-4a56-a42c-c7c96bc9cd54"],
+        "function_name": "print",
+        "function_module": "builtins",
+        "output_ids": ["DATASOURCE_foo_4d9923b8-eb9f-4f3c-8055-3a1ce8bee309"],
+    }
+
+
+@pytest.fixture
+def sequence_data():
+    return {
+        "name": "foo",
+        "task_ids": ["TASK_foo_3b888e17-1974-4a56-a42c-c7c96bc9cd54"],
+    }
+
+
+@pytest.fixture
+def scenario_data():
+    return {
+        "name": "foo",
+        "sequence_ids": ["SEQUENCE_foo_3b888e17-1974-4a56-a42c-c7c96bc9cd54"],
+        "properties": {},
+    }
+
+
+@pytest.fixture
+def default_datanode():
+    return InMemoryDataNode(
+        "input_ds",
+        Scope.SCENARIO,
+        DataNodeId("f"),
+        "my name",
+        "owner_id",
+        properties={"default_data": [1, 2, 3, 4, 5, 6]},
+    )
+
+
+@pytest.fixture
+def default_df_datanode():
+    return InMemoryDataNode(
+        "input_ds",
+        Scope.SCENARIO,
+        DataNodeId("id_uio2"),
+        "my name",
+        "owner_id",
+        properties={"default_data": pd.DataFrame([{"a": 1, "b": 2}, {"a": 3, "b": 4}, {"a": 5, "b": 6}])},
+    )
+
+
+@pytest.fixture
+def default_datanode_config():
+    return Config.configure_data_node(f"taipy_{uuid.uuid4().hex}", "in_memory", Scope.SCENARIO)
+
+
+@pytest.fixture
+def default_datanode_config_list():
+    configs = []
+    for i in range(10):
+        configs.append(Config.configure_data_node(id=f"ds_{i}", storage_type="in_memory", scope=Scope.SCENARIO))
+    return configs
+
+
+def __default_task():
+    input_ds = InMemoryDataNode(
+        "input_ds",
+        Scope.SCENARIO,
+        DataNodeId("id_uio"),
+        "my name",
+        "owner_id",
+        properties={"default_data": "In memory Data Source"},
+    )
+
+    output_ds = InMemoryDataNode(
+        "output_ds",
+        Scope.SCENARIO,
+        DataNodeId("id_uio"),
+        "my name",
+        "owner_id",
+        properties={"default_data": "In memory Data Source"},
+    )
+    return Task(
+        config_id="foo",
+        properties={},
+        function=print,
+        input=[input_ds],
+        output=[output_ds],
+        id=None,
+    )
+
+
+@pytest.fixture
+def default_task():
+    return __default_task()
+
+
+@pytest.fixture
+def default_task_config():
+    return Config.configure_task("task1", print, [], [])
+
+
+@pytest.fixture
+def default_task_config_list():
+    configs = []
+    for i in range(10):
+        configs.append(Config.configure_task(f"task_{i}", print, [], []))
+    return configs
+
+
+def __default_sequence():
+    return Sequence(properties={"name": "foo"}, tasks=[__default_task()], sequence_id="SEQUENCE_foo_SCENARIO_acb")
+
+
+def __task_config():
+    return Config.configure_task("task1", print, [], [])
+
+
+@pytest.fixture
+def default_sequence():
+    return __default_sequence()
+
+
+@pytest.fixture
+def default_scenario_config():
+    task_config = __task_config()
+    scenario_config = Config.configure_scenario(
+        f"taipy_{uuid.uuid4().hex}",
+        [task_config],
+    )
+    scenario_config.add_sequences({"sequence": [task_config]})
+    return scenario_config
+
+
+@pytest.fixture
+def default_scenario_config_list():
+    configs = []
+    for _ in range(10):
+        task_config = Config.configure_task(f"taipy_{uuid.uuid4().hex}", print)
+        scenario_config = Config.configure_scenario(
+            f"taipy_{uuid.uuid4().hex}",
+            [task_config],
+        )
+        scenario_config.add_sequences({"sequence": [task_config]})
+        configs.append(scenario_config)
+    return configs
+
+
+@pytest.fixture
+def default_scenario():
+    return Scenario(config_id="foo", properties={}, tasks=[__default_task()], scenario_id="SCENARIO_scenario_id")
+
+
+def __create_cycle(name="foo"):
+    now = datetime.now()
+    return Cycle(
+        name=name,
+        frequency=Frequency.DAILY,
+        properties={},
+        creation_date=now,
+        start_date=now,
+        end_date=now + timedelta(days=5),
+    )
+
+
+@pytest.fixture
+def create_cycle_list():
+    cycles = []
+    manager = _CycleManager
+    for i in range(10):
+        c = __create_cycle(f"cycle_{1}")
+        manager._set(c)
+    return cycles
+
+
+@pytest.fixture
+def cycle_data():
+    return {
+        "name": "foo",
+        "frequency": "daily",
+        "properties": {},
+        "creation_date": "2022-02-03T22:17:27.317114",
+        "start_date": "2022-02-03T22:17:27.317114",
+        "end_date": "2022-02-08T22:17:27.317114",
+    }
+
+
+@pytest.fixture
+def default_cycle():
+    return __create_cycle()
+
+
+def __create_job():
+    task_manager = _TaskManager
+    task = __default_task()
+    task_manager._set(task)
+    submit_id = f"SUBMISSION_{str(uuid.uuid4())}"
+    return Job(id=JobId(f"JOB_{uuid.uuid4()}"), task=task, submit_id=submit_id, submit_entity_id=task.id)
+
+
+@pytest.fixture
+def default_job():
+    return __create_job()
+
+
+@pytest.fixture
+def create_job_list():
+    jobs = []
+    manager = _JobManager
+    for i in range(10):
+        c = __create_job()
+        manager._set(c)
+    return jobs
+
+
+@pytest.fixture(scope="function", autouse=True)
+def cleanup_files():
+    Config.unblock_update()
+    if os.path.exists(".data"):
+        shutil.rmtree(".data")

+ 9 - 0
tests/rest/json/expected/cycle.json

@@ -0,0 +1,9 @@
+{
+    "id": "CYCLE_Frequency.DAILY_2022-03-31T215052.349698_4dfea10a-605f-4d02-91cd-cc8c43cd7d2f",
+    "name": "Frequency.DAILY_2022-03-31T21:50:52.349698",
+    "frequency": "<Frequency.DAILY: 1>",
+    "properties": {},
+    "creation_date": "2022-03-31T21:50:52.349698",
+    "start_date": "2022-03-31T00:00:00",
+    "end_date": "2022-03-31T23:59:59.999999"
+}

+ 18 - 0
tests/rest/json/expected/datanode.json

@@ -0,0 +1,18 @@
+{
+    "id": "DATANODE_day_8c0595aa-2dcf-4080-aa78-2eebd7619c9b",
+    "config_id": "day",
+    "scope": "<Scope.SCENARIO: 2>",
+    "storage_type": "pickle",
+    "name": "DATANODE_day_8c0595aa-2dcf-4080-aa78-2eebd7619c9b",
+    "owner_id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "parent_ids": ["TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70edab9189"],
+    "last_edit_date": null,
+    "job_ids": [],
+    "validity_days": null,
+    "validity_seconds": null,
+    "edit_in_progress": false,
+    "cacheable": false,
+    "data_node_properties": {
+    },
+    "version": "1.0"
+}

+ 15 - 0
tests/rest/json/expected/job.json

@@ -0,0 +1,15 @@
+{
+    "id": "JOB_602e112d-2bfa-4813-8d02-79da294fe56f",
+    "task_id": "TASK_evaluate_task_bf7796b3-e248-4e44-a87f-420b748ea461",
+    "status": "<Status.BLOCKED: 2>",
+    "force": false,
+    "creation_date": "2022-03-31T22:00:23.710789",
+    "subscribers": [
+        {
+            "fct_name": "_Orchestrator._on_status_change",
+            "fct_module": "taipy.core._orchestrator._orchestrator"
+        }
+    ],
+    "stacktrace": [],
+    "version": "1.0"
+}

+ 19 - 0
tests/rest/json/expected/scenario.json

@@ -0,0 +1,19 @@
+{
+    "id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "config_id": "scenario",
+    "tasks": [
+        "TASK_forecast_task_7e1bac33-15f1-4cfd-8702-17adeee902cb",
+        "TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70eddc6973"
+    ],
+    "additional_data_nodes": [],
+    "sequences": [
+        "SEQUENCE_sequence_0e1c5dd3-9896-4221-bfc8-6924acd11147"
+    ],
+    "properties": {},
+    "creation_date": "2022-03-31T21:50:52.360872",
+    "primary_scenario": true,
+    "subscribers": [],
+    "tags": [],
+    "cycle": "CYCLE_Frequency.DAILY_2022-03-31T215052.349698_4dfea10a-605f-4d02-91cd-cc8c43cd7d2f",
+    "version": "1.0"
+}

+ 13 - 0
tests/rest/json/expected/sequence.json

@@ -0,0 +1,13 @@
+{
+    "id": "SEQUENCE_sequence_0e1c5dd3-9896-4221-bfc8-6924pjg11147",
+    "owner_id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "parent_ids": [],
+    "config_id": "sequence",
+    "properties": {},
+    "tasks": [
+        "TASK_forecast_task_7e1bac33-15f1-4cfd-8702-17adeee902cb",
+        "TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70eddc6973"
+    ],
+    "subscribers": [],
+    "version": "1.0"
+}

+ 17 - 0
tests/rest/json/expected/task.json

@@ -0,0 +1,17 @@
+{
+    "id": "TASK_evaluate_task_31f1ecdf-2b75-4cd8-9509-9e70eddc6973",
+    "owner_id": "SCENARIO_scenario_a9c3eea2-2af3-4a85-a0c3-ef98ff5bd586",
+    "parent_ids": ["SEQUENCE_sequence_0e1c5aa9-9896-4221-bfc8-6924acd11147"],
+    "config_id": "evaluate_task",
+    "input_ids": [
+        "DATANODE_historical_temperature_5525cf20-c3a3-42ce-a1c9-88e988bb49f9",
+        "DATANODE_forecasts_c1d9ace5-6099-44ce-a90f-6518db37bbf4",
+        "DATANODE_day_8c0595aa-2dcf-4080-aa78-2eebd7619c9b"
+    ],
+    "function_name": "evaluate",
+    "function_module": "tests.setup.shared.algorithms",
+    "output_ids": [
+        "DATANODE_evaluation_a208b475-11ba-452f-aec6-077e46ced49b"
+    ],
+    "version": "1.0"
+}

+ 10 - 0
tests/rest/setup/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

BIN
tests/rest/setup/my_model.p


+ 10 - 0
tests/rest/setup/shared/__init__.py

@@ -0,0 +1,10 @@
+# Copyright 2023 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.

+ 55 - 0
tests/rest/setup/shared/algorithms.py

@@ -0,0 +1,55 @@
+# Copyright 2023 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 pickle
+import random
+from datetime import datetime, timedelta
+from typing import Any, Dict
+
+import pandas as pd
+
+n_predictions = 14
+
+
+def forecast(model, date: datetime):
+    dates = [date + timedelta(days=i) for i in range(n_predictions)]
+    forecasts = [f + random.uniform(0, 2) for f in model.forecast(len(dates))]
+    days = [str(dt.date()) for dt in dates]
+    res = {"Date": days, "Forecast": forecasts}
+    return pd.DataFrame.from_dict(res)
+
+
+def evaluate(cleaned: pd.DataFrame, forecasts: pd.DataFrame, date: datetime) -> Dict[str, Any]:
+    cleaned = cleaned[cleaned["Date"].isin(forecasts["Date"].tolist())]
+    forecasts_as_series = pd.Series(forecasts["Forecast"].tolist(), name="Forecast")
+    res = pd.concat([cleaned.reset_index(), forecasts_as_series], axis=1)
+    res["Delta"] = abs(res["Forecast"] - res["Value"])
+
+    return {
+        "Date": date,
+        "Dataframe": res,
+        "Mean_absolute_error": res["Delta"].mean(),
+        "Relative_error": (res["Delta"].mean() * 100) / res["Value"].mean(),
+    }
+
+
+if __name__ == "__main__":
+    model = pickle.load(open("../my_model.p", "rb"))
+    day = datetime(2020, 1, 25)
+    forecasts = forecast(model, day)
+
+    historical_temperature = pd.read_csv("../historical_temperature.csv")
+    evaluation = evaluate(historical_temperature, forecasts, day)
+
+    print(evaluation["Dataframe"])
+    print()
+    print(f'Mean absolute error : {evaluation["Mean_absolute_error"]}')
+    print(f'Relative error in %: {evaluation["Relative_error"]}')

+ 42 - 0
tests/rest/setup/shared/config.py

@@ -0,0 +1,42 @@
+# Copyright 2023 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.
+
+from taipy.core import Config, Frequency
+
+from .algorithms import evaluate, forecast
+
+model_cfg = Config.configure_data_node("model", path="my_model.p", storage_type="pickle")
+
+day_cfg = Config.configure_data_node(id="day")
+forecasts_cfg = Config.configure_data_node(id="forecasts")
+forecast_task_cfg = Config.configure_task(
+    id="forecast_task",
+    input=[model_cfg, day_cfg],
+    function=forecast,
+    output=forecasts_cfg,
+)
+
+historical_temperature_cfg = Config.configure_data_node(
+    "historical_temperature",
+    storage_type="csv",
+    path="historical_temperature.csv",
+    has_header=True,
+)
+evaluation_cfg = Config.configure_data_node("evaluation")
+evaluate_task_cfg = Config.configure_task(
+    "evaluate_task",
+    input=[historical_temperature_cfg, forecasts_cfg, day_cfg],
+    function=evaluate,
+    output=evaluation_cfg,
+)
+
+scenario_cfg = Config.configure_scenario("scenario", [forecast_task_cfg, evaluate_task_cfg], frequency=Frequency.DAILY)
+scenario_cfg.add_sequences({"sequence": [forecast_task_cfg, evaluate_task_cfg]})

+ 62 - 0
tests/rest/test_cycle.py

@@ -0,0 +1,62 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+from flask import url_for
+
+
+def test_get_cycle(client, default_cycle):
+    # test 404
+    cycle_url = url_for("api.cycle_by_id", cycle_id="foo")
+    rep = client.get(cycle_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.cycle._cycle_manager._CycleManager._get") as manager_mock:
+        manager_mock.return_value = default_cycle
+
+        # test get_cycle
+        rep = client.get(url_for("api.cycle_by_id", cycle_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_cycle(client):
+    # test 404
+    cycle_url = url_for("api.cycle_by_id", cycle_id="foo")
+    rep = client.get(cycle_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.cycle._cycle_manager._CycleManager._delete"), mock.patch(
+        "taipy.core.cycle._cycle_manager._CycleManager._get"
+    ):
+        # test get_cycle
+        rep = client.delete(url_for("api.cycle_by_id", cycle_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_cycle(client, cycle_data):
+    # without config param
+    cycles_url = url_for("api.cycles")
+    data = {"bad": "data"}
+    rep = client.post(cycles_url, json=data)
+    assert rep.status_code == 400
+
+    rep = client.post(cycles_url, json=cycle_data)
+    assert rep.status_code == 201
+
+
+def test_get_all_cycles(client, create_cycle_list):
+    cycles_url = url_for("api.cycles")
+    rep = client.get(cycles_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10

+ 111 - 0
tests/rest/test_datanode.py

@@ -0,0 +1,111 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+import pytest
+from flask import url_for
+
+
+def test_get_datanode(client, default_datanode):
+    # test 404
+    user_url = url_for("api.datanode_by_id", datanode_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.data._data_manager._DataManager._get") as manager_mock:
+        manager_mock.return_value = default_datanode
+        # test get_datanode
+        rep = client.get(url_for("api.datanode_by_id", datanode_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_datanode(client):
+    # test 404
+    user_url = url_for("api.datanode_by_id", datanode_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.data._data_manager._DataManager._delete"), mock.patch(
+        "taipy.core.data._data_manager._DataManager._get"
+    ):
+        # test get_datanode
+        rep = client.delete(url_for("api.datanode_by_id", datanode_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_datanode(client, default_datanode_config):
+    # without config param
+    datanodes_url = url_for("api.datanodes")
+    rep = client.post(datanodes_url)
+    assert rep.status_code == 400
+
+    # config does not exist
+    datanodes_url = url_for("api.datanodes", config_id="foo")
+    rep = client.post(datanodes_url)
+    assert rep.status_code == 404
+
+    with mock.patch("src.taipy.rest.api.resources.datanode.DataNodeList.fetch_config") as config_mock:
+        config_mock.return_value = default_datanode_config
+        datanodes_url = url_for("api.datanodes", config_id="bar")
+        rep = client.post(datanodes_url)
+        assert rep.status_code == 201
+
+
+def test_get_all_datanodes(client, default_datanode_config_list):
+    for ds in range(10):
+        with mock.patch("src.taipy.rest.api.resources.datanode.DataNodeList.fetch_config") as config_mock:
+            config_mock.return_value = default_datanode_config_list[ds]
+            datanodes_url = url_for("api.datanodes", config_id=config_mock.name)
+            client.post(datanodes_url)
+
+    rep = client.get(datanodes_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10
+
+
+def test_read_datanode(client, default_df_datanode):
+    with mock.patch("taipy.core.data._data_manager._DataManager._get") as config_mock:
+        config_mock.return_value = default_df_datanode
+
+        # without operators
+        datanodes_url = url_for("api.datanode_reader", datanode_id="foo")
+        rep = client.get(datanodes_url, json={})
+        assert rep.status_code == 200
+
+        # Without operators and body
+        rep = client.get(datanodes_url)
+        assert rep.status_code == 200
+
+        # TODO: Revisit filter test
+        # operators = {"operators": [{"key": "a", "value": 5, "operator": "LESS_THAN"}]}
+        # rep = client.get(datanodes_url, json=operators)
+        # assert rep.status_code == 200
+
+
+def test_write_datanode(client, default_datanode):
+    with mock.patch("taipy.core.data._data_manager._DataManager._get") as config_mock:
+        config_mock.return_value = default_datanode
+        # Get DataNode
+        datanodes_read_url = url_for("api.datanode_reader", datanode_id=default_datanode.id)
+        rep = client.get(datanodes_read_url, json={})
+        assert rep.status_code == 200
+        assert rep.json == {"data": [1, 2, 3, 4, 5, 6]}
+
+        datanodes_write_url = url_for("api.datanode_writer", datanode_id=default_datanode.id)
+        rep = client.put(datanodes_write_url, json=[1, 2, 3])
+        assert rep.status_code == 200
+
+        rep = client.get(datanodes_read_url, json={})
+        assert rep.status_code == 200
+        assert rep.json == {"data": [1, 2, 3]}

+ 100 - 0
tests/rest/test_end_to_end.py

@@ -0,0 +1,100 @@
+# Copyright 2023 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
+from typing import Dict
+
+from flask import url_for
+
+
+def create_and_submit_scenario(config_id: str, client) -> Dict:
+    response = client.post(url_for("api.scenarios", config_id=config_id))
+    assert response.status_code == 201
+
+    scenario = response.json.get("scenario")
+    assert (set(scenario) - set(json.load(open("tests/rest/json/expected/scenario.json")))) == set()
+
+    response = client.post(url_for("api.scenario_submit", scenario_id=scenario.get("id")))
+    assert response.status_code == 200
+
+    return scenario
+
+
+def get(url, name, client) -> Dict:
+    response = client.get(url)
+    returned_data = response.json.get(name)
+
+    assert (set(returned_data) - set(json.load(open(f"tests/rest/json/expected/{name}.json")))) == set()
+
+    return returned_data
+
+
+def get_assert_status(url, client, status_code) -> None:
+    response = client.get(url)
+    assert response.status_code == status_code
+
+
+def get_all(url, expected_quantity, client):
+    response = client.get(url)
+
+    assert len(response.json) == expected_quantity
+
+
+def delete(url, client):
+    response = client.delete(url)
+
+    assert response.status_code == 200
+
+
+def test_end_to_end(client, setup_end_to_end):
+    # Create Scenario: Should also create all of its dependencies(sequences, tasks, datanodes, etc)
+    scenario = create_and_submit_scenario("scenario", client)
+
+    # Get other models and verify if they return the necessary fields
+    cycle = get(url_for("api.cycle_by_id", cycle_id=scenario.get("cycle")), "cycle", client)
+    sequence = get(
+        url_for("api.sequence_by_id", sequence_id=f"SEQUENCE_sequence_{scenario['id']}"),
+        "sequence",
+        client,
+    )
+    task = get(url_for("api.task_by_id", task_id=sequence.get("tasks")[0]), "task", client)
+    datanode = get(
+        url_for("api.datanode_by_id", datanode_id=task.get("input_ids")[0]),
+        "datanode",
+        client,
+    )
+    # Get All
+    get_all(url_for("api.scenarios"), 1, client)
+    get_all(url_for("api.cycles"), 1, client)
+    get_all(url_for("api.sequences"), 1, client)
+    get_all(url_for("api.tasks"), 2, client)
+    get_all(url_for("api.datanodes"), 5, client)
+    get_all(url_for("api.jobs"), 2, client)
+
+    # Delete entities
+    delete(url_for("api.cycle_by_id", cycle_id=cycle.get("id")), client)
+    delete(url_for("api.sequence_by_id", sequence_id=sequence.get("id")), client)
+    delete(url_for("api.task_by_id", task_id=task.get("id")), client)
+    delete(url_for("api.datanode_by_id", datanode_id=datanode.get("id")), client)
+
+    # Check status code
+    # Non-existing entities should return 404
+    get_assert_status(url_for("api.cycle_by_id", cycle_id=9999999), client, 404)
+    get_assert_status(url_for("api.scenario_by_id", scenario_id=9999999), client, 404)
+    get_assert_status(url_for("api.sequence_by_id", sequence_id=9999999), client, 404)
+    get_assert_status(url_for("api.task_by_id", task_id=9999999), client, 404)
+    get_assert_status(url_for("api.datanode_by_id", datanode_id=9999999), client, 404)
+
+    # Check URL with and without trailing slashes
+    url_with_slash = url_for("api.scenarios")
+    url_without_slash = url_for("api.scenarios")[:-1]
+    get_all(url_with_slash, 1, client)
+    get_all(url_without_slash, 1, client)

+ 83 - 0
tests/rest/test_job.py

@@ -0,0 +1,83 @@
+# Copyright 2023 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.
+
+from unittest import mock
+
+from flask import url_for
+
+
+def test_get_job(client, default_job):
+    # test 404
+    user_url = url_for("api.job_by_id", job_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.job._job_manager._JobManager._get") as manager_mock:
+        manager_mock.return_value = default_job
+
+        # test get_job
+        rep = client.get(url_for("api.job_by_id", job_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_delete_job(client):
+    # test 404
+    user_url = url_for("api.job_by_id", job_id="foo")
+    rep = client.get(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.job._job_manager._JobManager._delete"), mock.patch(
+        "taipy.core.job._job_manager._JobManager._get"
+    ):
+        # test get_job
+        rep = client.delete(url_for("api.job_by_id", job_id="foo"))
+        assert rep.status_code == 200
+
+
+def test_create_job(client, default_task_config):
+    # without config param
+    jobs_url = url_for("api.jobs")
+    rep = client.post(jobs_url)
+    assert rep.status_code == 400
+
+    with mock.patch("src.taipy.rest.api.resources.job.JobList.fetch_config") as config_mock:
+        config_mock.return_value = default_task_config
+        jobs_url = url_for("api.jobs", task_id="foo")
+        rep = client.post(jobs_url)
+        assert rep.status_code == 201
+
+
+def test_get_all_jobs(client, create_job_list):
+    jobs_url = url_for("api.jobs")
+    rep = client.get(jobs_url)
+    assert rep.status_code == 200
+
+    results = rep.get_json()
+    assert len(results) == 10
+
+
+def test_cancel_job(client, default_job):
+    # test 404
+    from taipy.core._orchestrator._orchestrator_factory import _OrchestratorFactory
+
+    _OrchestratorFactory._build_orchestrator()
+    _OrchestratorFactory._build_dispatcher()
+
+    user_url = url_for("api.job_cancel", job_id="foo")
+    rep = client.post(user_url)
+    assert rep.status_code == 404
+
+    with mock.patch("taipy.core.job._job_manager._JobManager._get") as manager_mock:
+        manager_mock.return_value = default_job
+
+        # test get_job
+        rep = client.post(url_for("api.job_cancel", job_id="foo"))
+        assert rep.status_code == 200

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác