Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/develop' into feature/merge-core

Joao Andre 1 rok pred
rodič
commit
f8a09a9af8
97 zmenil súbory, kde vykonal 6260 pridanie a 65 odobranie
  1. 2 1
      .github/workflows/tests.yml
  2. 51 50
      README.md
  3. BIN
      readme_img/gui_creation.webp
  4. BIN
      readme_img/readme_cloud_demo.gif
  5. BIN
      readme_img/readme_core_intro.gif
  6. BIN
      readme_img/readme_demo_studio.gif
  7. BIN
      readme_img/readme_exec_g.png
  8. BIN
      readme_img/readme_exec_graph.png
  9. BIN
      readme_img/readme_gui_app.gif
  10. BIN
      readme_img/readme_gui_intro.gif
  11. BIN
      readme_img/readme_studio.gif
  12. BIN
      readme_img/scenario_and_data_mgt.gif
  13. BIN
      readme_img/tiny_demo_readme.gif
  14. 1 1
      src/taipy/_run.py
  15. 2 0
      src/taipy/rest/.coveragerc
  16. 4 0
      src/taipy/rest/.dockerignore
  17. 24 0
      src/taipy/rest/.flake8
  18. 4 0
      src/taipy/rest/.flaskenv
  19. 38 0
      src/taipy/rest/.github/workflows/codeql-analysis.yml
  20. 27 0
      src/taipy/rest/.github/workflows/coverage.yml
  21. 89 0
      src/taipy/rest/.github/workflows/publish.yml
  22. 141 0
      src/taipy/rest/.github/workflows/release-dev.yml
  23. 69 0
      src/taipy/rest/.github/workflows/release.yml
  24. 32 0
      src/taipy/rest/.github/workflows/setuptools.yml
  25. 52 0
      src/taipy/rest/.github/workflows/tests.yml
  26. 141 0
      src/taipy/rest/.gitignore
  27. 9 0
      src/taipy/rest/.isort.cfg
  28. 10 0
      src/taipy/rest/.license-header
  29. 41 0
      src/taipy/rest/.pre-commit-config.yaml
  30. 2 0
      src/taipy/rest/.testenv
  31. 128 0
      src/taipy/rest/CODE_OF_CONDUCT.md
  32. 135 0
      src/taipy/rest/CONTRIBUTING.md
  33. 97 0
      src/taipy/rest/INSTALLATION.md
  34. 21 0
      src/taipy/rest/LICENSE
  35. 1 0
      src/taipy/rest/MANIFEST.in
  36. 28 0
      src/taipy/rest/Pipfile
  37. 88 0
      src/taipy/rest/README.md
  38. 26 0
      src/taipy/rest/__init__.py
  39. 12 0
      src/taipy/rest/_init.py
  40. 14 0
      src/taipy/rest/api/__init__.py
  41. 108 0
      src/taipy/rest/api/error_handler.py
  42. 10 0
      src/taipy/rest/api/exceptions/__init__.py
  43. 25 0
      src/taipy/rest/api/exceptions/exceptions.py
  44. 10 0
      src/taipy/rest/api/middlewares/__init__.py
  45. 34 0
      src/taipy/rest/api/middlewares/_middleware.py
  46. 38 0
      src/taipy/rest/api/resources/__init__.py
  47. 420 0
      src/taipy/rest/api/resources/cycle.py
  48. 662 0
      src/taipy/rest/api/resources/datanode.py
  49. 287 0
      src/taipy/rest/api/resources/job.py
  50. 527 0
      src/taipy/rest/api/resources/scenario.py
  51. 292 0
      src/taipy/rest/api/resources/sequence.py
  52. 278 0
      src/taipy/rest/api/resources/task.py
  53. 43 0
      src/taipy/rest/api/schemas/__init__.py
  54. 25 0
      src/taipy/rest/api/schemas/cycle.py
  55. 101 0
      src/taipy/rest/api/schemas/datanode.py
  56. 27 0
      src/taipy/rest/api/schemas/job.py
  57. 27 0
      src/taipy/rest/api/schemas/scenario.py
  58. 25 0
      src/taipy/rest/api/schemas/sequence.py
  59. 24 0
      src/taipy/rest/api/schemas/task.py
  60. 213 0
      src/taipy/rest/api/views.py
  61. 59 0
      src/taipy/rest/app.py
  62. 10 0
      src/taipy/rest/commons/__init__.py
  63. 103 0
      src/taipy/rest/commons/apispec.py
  64. 28 0
      src/taipy/rest/commons/encoder.py
  65. 50 0
      src/taipy/rest/commons/pagination.py
  66. 22 0
      src/taipy/rest/commons/templates/redoc.j2
  67. 51 0
      src/taipy/rest/commons/templates/swagger.j2
  68. 28 0
      src/taipy/rest/commons/to_from_model.py
  69. 8 0
      src/taipy/rest/contributors.txt
  70. 20 0
      src/taipy/rest/extensions.py
  71. 45 0
      src/taipy/rest/rest.py
  72. 58 0
      src/taipy/rest/setup.py
  73. 44 0
      src/taipy/rest/tox.ini
  74. 1 0
      src/taipy/rest/version.json
  75. 22 0
      src/taipy/rest/version.py
  76. 14 13
      tests/core/test_core.py
  77. 10 0
      tests/rest/__init__.py
  78. 319 0
      tests/rest/conftest.py
  79. 9 0
      tests/rest/json/expected/cycle.json
  80. 18 0
      tests/rest/json/expected/datanode.json
  81. 15 0
      tests/rest/json/expected/job.json
  82. 19 0
      tests/rest/json/expected/scenario.json
  83. 13 0
      tests/rest/json/expected/sequence.json
  84. 17 0
      tests/rest/json/expected/task.json
  85. 10 0
      tests/rest/setup/__init__.py
  86. BIN
      tests/rest/setup/my_model.p
  87. 10 0
      tests/rest/setup/shared/__init__.py
  88. 55 0
      tests/rest/setup/shared/algorithms.py
  89. 42 0
      tests/rest/setup/shared/config.py
  90. 62 0
      tests/rest/test_cycle.py
  91. 111 0
      tests/rest/test_datanode.py
  92. 100 0
      tests/rest/test_end_to_end.py
  93. 83 0
      tests/rest/test_job.py
  94. 59 0
      tests/rest/test_middleware.py
  95. 90 0
      tests/rest/test_scenario.py
  96. 102 0
      tests/rest/test_sequence.py
  97. 88 0
      tests/rest/test_task.py

+ 2 - 1
.github/workflows/tests.yml

@@ -8,8 +8,9 @@ on:
 
 jobs:
   backend:
-    timeout-minutes: 20
+    timeout-minutes: 40
     strategy:
+      fail-fast: false
       matrix:
         python-versions: ['3.8', '3.9', '3.10', '3.11']
         os: [ubuntu-latest, windows-latest, macos-latest]

+ 51 - 50
README.md

@@ -3,7 +3,7 @@
 
 <img align="left" src="readme_img/readme_logo.png" alt="Taipy Logo" width="20%" ></img>
 <br>
-#  Welcome to Taipy
+#  Taipy -Your Web Application Builder. Pure Python.
 <p align="left">
     <a href="https://pypi.python.org/pypi/taipy/" alt="Taipy version">
         <img alt="PyPI" src="https://img.shields.io/pypi/v/taipy.svg?label=pip&logo=PyPI&color=ff462b&labelColor=283282"></a>
@@ -17,28 +17,21 @@
 
 <br>
 
-###  <div align="left">Turns Data and AI algorithms into full web applications in no time.
-###  How? Taipy GUI with Taipy Core pops out as a 360° platform to build production-ready web applications</div>
-
+### Taipy is an open-source Python library for building your web application front-end & back-end. 
+### Turns data and AI algorithms into production-ready web applications in no time. 
 
+### <div align="center"> <a href="https://docs.taipy.io/en/latest/">Documentation</a> • <a href="https://discord.com/invite/SJyz2VJGxV">Join our Discord</a> • <a href="https://docs.taipy.io/en/latest/knowledge_base/demos/">Check out Taipy Applications</a>
+</div>
 
-<br>
-<br>
-
-###  <div align="left">*Open Source, 100% Python*</div>
-
-
-<br>
-<br>
 <br>
 
 #  <div align="center"> 📊 We make both ends meet ⚙️ </div>
 <br>
  <div align="center">
 
-| TAIPY GUI - the frond-end  | TAIPY Core - the back-end |
+| User Interface Generation  | Scenario and Data Management |
 | --------  | -------- |
-|<img src="readme_img/readme_gui_intro.gif" alt="Taipy GUI Animation"  width="100%"/> | <img src="readme_img/readme_core_intro.gif" alt="Taipy Core Animation"  width="100%"/>
+|<img src="readme_img/gui_creation.webp" alt="Interface Animation"  width="850px" height="250px" /> | <img src="readme_img/scenario_and_data_mgt.gif" alt="Back-End Animation"  width="100%"/>
 
 
 </div>
@@ -66,19 +59,18 @@ Join our [Discord](https://discord.gg/XcFhrJZru3) to give us feedback, share you
 
 ## Ready, Set, GUI
 
-### Tiny Taipy GUI Demo
+### Tiny User Interface Demo
 
 ```python
 from taipy import Gui
 
 excitement_page = """
 # Welcome to Taipy
-## Getting started with Taipy GUI
 ### How excited are you to try Taipy?
 
 <|{excitement}|slider|min=1|max=100|>
 
-My excitement level: <|{excitement}|text|>
+My excitement level: <|{excitement}|>
 """
 excitement = 100
 
@@ -87,20 +79,23 @@ Gui(page=excitement_page).run()
 *RUN*🏃🏽‍♀️
 <div align="center">🎊 TA-DA! 🎊</div>
 <br>
-<div align="center"><img src="readme_img/readme_gui_app.gif" width="60%" alt="GUI demo"></img></div>
+<div align="center"><img src="readme_img/tiny_demo_readme.gif" width="50%" alt="Tiny Demo"></img></div>
 
-<br>
 <br>
 
-### <div align="center">*Find out more*</div>
-*<div align="center">Check out our [Getting Started](https://docs.taipy.io/en/latest/getting_started/getting-started-gui/) and [Documentation](https://docs.taipy.io/en/latest/manuals/gui/)</div>*
+***<div align="center">Check out our [Getting Started](https://docs.taipy.io/en/latest/getting_started/) and [Documentation](https://docs.taipy.io/en/latest/manuals/gui/)</div>***
 
 <br>
 <br>
 
-## EN-CORE?
+## Scenario and Data Management ⚙️
+
+**<div align="left">Let's create a *Scenario* in Taipy to filter movie data based on the genre you choose. This scenario models a simple pipeline. It is submitted (for execution) each time the genre selection changes and outputs the seven most popular movies of that genre. </div>**
+
+<br>
+
+<div align="center"> ⚠️ Here, the back-end involves the execution of a very simple pipeline (made of a single task). Note that Taipy is designed to build much more complex pipelines 🚀 (with many tasks!) </div>
 
-#### <div align="center">Let's create a back-end execution, also called *scenario* using Taipy Core. Our scenario will filter movie data based on the genre you choose. This scenario will be submitted (i.e., executed) each time the genre selection changes and output the seven most popular movies of that genre. </div>
 <br>
 
 *Here is our filter function: a standard Python function that is used by the unique task in the scenario*
@@ -113,62 +108,54 @@ def filter_genre(initial_dataset: pd.DataFrame, selected_genre):
 
 *This is the execution graph of the scenario we are implementing*
 
-<div align="center"><img src="readme_img/readme_exec_g.png" alt="Taipy Core Graph"  width="60%"/></div>
+<div align="center"><img src="readme_img/readme_exec_graph.png" alt="Demo Execution Graph"  width="50%"/></div>
 
 
 ### Taipy Studio - The easy peasy way
-*You can use the Taipy Studio extension in VSCode to configure your sequence with no code*
+*You can use the Taipy Studio extension in Visual Studio Code to configure your scenario with no code*
 
-<div align="center"><img src="readme_img/readme_studio.gif" width="80%" alt="GUI demo"></img></div>
+<div align="center"><img src="readme_img/readme_demo_studio.gif" width="80%" alt="Demo Studio Gif"></img></div>
 
 *Your configuration is automatically saved as a TOML file*
 
-<br>
 <br>
 
-### <div align="center">*Find out more*</div>
-*<div align="center">Check out our [Getting Started](https://docs.taipy.io/en/latest/getting_started/getting-started-core/) and [Documentation](https://docs.taipy.io/en/latest/manuals/studio/) </div>*
+***<div align="center">Check out our [Documentation](https://docs.taipy.io/en/latest/manuals/studio/) </div>***
 
-<br>
-<br>
 <br>
 <br>
 
-### Taipy Core - a walk on the code side
+### Taipy Scenario & Data Management - a walk on the code side
 <div align="left">For more advanced use cases or if you prefer coding your configurations instead of using Taipy Studio, Taipy has your back! </div>
 
 *<div align="left">Check out the movie genre demo scenario creation with this [Demo](https://www.taipy.io/project/movie-genre-selector/) </div>*
 
-<br>
-<br>
 <br>
 
-### <div align="center">*Find out more*</div>
-*<div align="center">Check out our [Getting Started](https://docs.taipy.io/en/latest/getting_started/getting-started-core/) and [Documentation](https://docs.taipy.io/en/latest/manuals/core/) </div>*
+***<div align="center">Check out our [Getting Started](https://docs.taipy.io/en/latest/getting_started/) and [Documentation](https://docs.taipy.io/en/latest/manuals/core/) </div>***
 
 <br>
 <br>
-<br>
 
 
-## Front-end ➕ Back-end
+## User Interface Generation ➕ Scenario & Data Management
 *Now, let's load this configuration and add a user interface on top for a 🎉FULL APPLICATION🎉*
 ```python
 import taipy as tp
 import pandas as pd
 from taipy import Config, Scope, Gui
 
-# TAIPY Core
+# Taipy Scenario & Data Management
 
 # Filtering function - task
 def filter_genre(initial_dataset: pd.DataFrame, selected_genre):
-    filtered_dataset = initial_dataset[initial_dataset['genres'].str.contains(selected_genre)]
-    filtered_data = filtered_dataset.nlargest(7, 'Popularity %')
+    filtered_dataset = initial_dataset[initial_dataset["genres"].str.contains(selected_genre)]
+    filtered_data = filtered_dataset.nlargest(7, "Popularity %")
     return filtered_data
 
 # Load the configuration made with Taipy Studio
-Config.load('config.toml')
-scenario_cfg = Config.scenarios['scenario']
+Config.load("config.toml")
+scenario_cfg = Config.scenarios["scenario"]
 
 # Start Taipy Core service
 tp.Core().run()
@@ -177,8 +164,8 @@ tp.Core().run()
 scenario = tp.create_scenario(scenario_cfg)
 
 
-# TAIPY GUI
-# Let's add Taipy GUI to our Taipy Core for a full application
+# Taipy User Interface
+# Let's add a GUI to our Scenario Management for a full application
 
 # Callback definition - submits scenario with genre selection
 def on_genre_selected(state):
@@ -188,13 +175,17 @@ def on_genre_selected(state):
 
 # Get list of genres
 genres = [
-    'Action', 'Adventure', 'Animation', 'Children', 'Comedy', 'Fantasy', 'IMAX'
-    'Romance','Sci-FI', 'Western', 'Crime', 'Mystery', 'Drama', 'Horror', 'Thriller', 'Film-Noir','War', 'Musical', 'Documentary'
+    "Action", "Adventure", "Animation", "Children", "Comedy", "Fantasy", "IMAX"
+    "Romance","Sci-FI", "Western", "Crime", "Mystery", "Drama", "Horror", "Thriller", "Film-Noir","War", "Musical", "Documentary"
     ]
 
 # Initialization of variables
-df = pd.DataFrame(columns=['Title', 'Popularity %'])
-selected_genre = None
+df = pd.DataFrame(columns=["Title", "Popularity %"])
+selected_genre = "Action"
+
+## Set initial value to Action
+def on_init(state):
+    on_genre_selected(state)
 
 # User interface definition
 my_page = """
@@ -216,12 +207,22 @@ Gui(page=my_page).run()
 
 <div align="center">🎊TA-DA!🎊</div>
 <br>
-<div align="center"><img src="readme_img/readme_app.gif" width="80%" alt="GUI demo"></img></div>
+<div align="center"><img src="readme_img/readme_app.gif" width="80%" alt="Image of a Taipy demonstration application"></img></div>
 
 <br>
 
 <br>
 
+## ☁️Taipy Cloud☁️
+With Taipy Cloud, you can deploy your Taipy applications in a *few clicks* and *for free*!
+
+<div align="center"><img src="readme_img/readme_cloud_demo.gif" alt="Demonstration of Taipy Cloud" width="60%" ></img></div>
+
+<br>
+<br>
+
+***<div align="center"> Click [here](https://www.taipy.io/taipy-cloud/) to get started for free </div>***
+
 <br>
 <br>
 

BIN
readme_img/gui_creation.webp


BIN
readme_img/readme_cloud_demo.gif


BIN
readme_img/readme_core_intro.gif


BIN
readme_img/readme_demo_studio.gif


BIN
readme_img/readme_exec_g.png


BIN
readme_img/readme_exec_graph.png


BIN
readme_img/readme_gui_app.gif


BIN
readme_img/readme_gui_intro.gif


BIN
readme_img/readme_studio.gif


BIN
readme_img/scenario_and_data_mgt.gif


BIN
readme_img/tiny_demo_readme.gif


+ 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",
+    ],
+)

+ 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

+ 14 - 13
tests/core/test_core.py

@@ -81,23 +81,24 @@ class TestCore:
             core.stop()
 
     def test_core_service_can_only_be_run_once(self):
-        core_instance_1 = Core()
-        core_instance_2 = Core()
-
-        core_instance_1.run()
+        with patch("sys.argv", ["prog"]):
+            core_instance_1 = Core()
+            core_instance_2 = Core()
 
-        with pytest.raises(CoreServiceIsAlreadyRunning):
             core_instance_1.run()
-        with pytest.raises(CoreServiceIsAlreadyRunning):
-            core_instance_2.run()
 
-        # Stop the Core service and run it again should work
-        core_instance_1.stop()
+            with pytest.raises(CoreServiceIsAlreadyRunning):
+                core_instance_1.run()
+            with pytest.raises(CoreServiceIsAlreadyRunning):
+                core_instance_2.run()
 
-        core_instance_1.run()
-        core_instance_1.stop()
-        core_instance_2.run()
-        core_instance_2.stop()
+            # Stop the Core service and run it again should work
+            core_instance_1.stop()
+
+            core_instance_1.run()
+            core_instance_1.stop()
+            core_instance_2.run()
+            core_instance_2.stop()
 
     def test_block_config_update_when_core_service_is_running_development_mode(self):
         _OrchestratorFactory._dispatcher = None

+ 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.

+ 319 - 0
tests/rest/conftest.py

@@ -0,0 +1,319 @@
+# 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()
+    Config.configure_core(repository_type="filesystem")
+
+    if os.path.exists(".data"):
+        shutil.rmtree(".data", ignore_errors=True)
+    if os.path.exists(".my_data"):
+        shutil.rmtree(".my_data", ignore_errors=True)

+ 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

+ 59 - 0
tests/rest/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
tests/rest/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
tests/rest/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
tests/rest/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