Repository: fast-aircraft-design/FAST-OAD (language: python)

Issue:
Custom configuration files could be associated to custom modules
**Is your feature request related to a problem? Please describe.**
FAST-OAD currently does not propose a way to provide a sample configuration file adapted to custom models.

**Describe the solution you'd like**
Developers of custom modules should have a way to add a sample configuration file to the provided models. The FAST-OAD command for generating a configuration file should propose a way to choose the desired sample configuration file.

Testing CLI
FAST-OAD uses [`click`](https://click.palletsprojects.com/en/8.0.x/) since PR #405. It should now be possible to build tests for the CLI, which should be done.


Discussion / hints:
It should also be possible to associate notebooks to a plugin


Fix patch:
diff --git a/.codacy.yml b/.codacy.yml
index d66860b9b..f35f10ed4 100644
--- a/.codacy.yml
+++ b/.codacy.yml
@@ -2,6 +2,7 @@
 exclude_paths:
   - '**/tests/**'
   - 'tests/**'
+  - '**/conftest.py'
   - 'docs/conf.py'
   - 'src/fastoad/api.py'
   - '.github/**'
\ No newline at end of file
diff --git a/.codeclimate.yml b/.codeclimate.yml
index 9bcc95fec..6031b5304 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -1,3 +1,10 @@
+version: "2"         # required to adjust maintainability checks
+checks:
+  method-complexity:
+    config:
+      threshold: 10
+exclude_patterns:
+  - "**/contest.py"
 plugins:
   pylint:
     enabled: true
diff --git a/docs/documentation/custom_modules/add_plugin.rst b/docs/documentation/custom_modules/add_plugin.rst
index a4110411b..a6e192e6f 100644
--- a/docs/documentation/custom_modules/add_plugin.rst
+++ b/docs/documentation/custom_modules/add_plugin.rst
@@ -4,30 +4,109 @@
 How to add custom OpenMDAO modules to FAST-OAD as a plugin
 ##########################################################
 
-Once you have created your custom modules for FAST-OAD (see :ref:`add-modules`),
+Once you have :ref:`created your custom modules <add-modules>` for FAST-OAD,
 you may want to share them with other users, which can be done in two ways:
 
     - Providing your code so they can copy it on their computer and have them set their
       :code:`custom_modules` field accordingly in their :ref:`configuration-file`.
     - Packaging your code as a FAST-OAD plugin and have them install it through :code:`pip`
-      or equivalent.
+      or equivalent. This is the subject of current chapter.
 
-To declare your custom modules as a FAST-OAD plugin, you have to package them the usual way
-and declare them as a plugin with :code:`fastoad_model` as plugin group name.
+A FAST-OAD plugin can provide additional FAST-OAD modules, Jupyter notebooks and configuration files:
+
+    - plugin-provided FAST-OAD modules are usable in :ref:`configuration files <configuration-file>`,
+      and can be :ref:`listed<get-module-list>` and :ref:`used<configuration-file-problem-definition>`
+      in the same way as native modules.
+    - Command line can be used by users to retrieve :ref:`notebooks<python-usage>` and
+      :ref:`configuration files<generate-conf-file>`.
+
+Plugin structure
+################
+In your source folder, a typical plugin structure would be like this::
+
+    my_package/
+    ├── __init__.py
+    ├── configurations/
+    │   ├── __init__.py
+    │   ├── configuration_1.yaml
+    │   └── configuration_2.yaml
+    ├── models/
+    │   ├── __init__.py
+    │   ├── my_model.py
+    │   └── some_subpackage/
+    │       ├── __init__.py
+    │       └── some_more_code.py
+    └── notebooks/
+        ├── __init__.py
+        ├── any_data/
+        │   ├── __init__.py
+        │   └── some_data.xml
+        ├── awesome_notebook.ipynb
+        └── good_notebook.ipynb
+
+As shown above, the expected structure is composed of Python **packages**, i.e. every folder should
+contain a :code:`__init__.py` file, **even if it contains only non-Python files** (e.g. data for notebooks).
+
+The root folder can be anywhere in your project structure, since plugin declaration will point to
+its location.
+
+Expected folders in a plugin package are:
+
+    - :code:`models`: contains Python code where FAST-OAD modules are :ref:`registered<add-modules>`.
+    - :code:`configurations`: contains only configuration files in YAML format. No sub-folder is
+      allowed. These configuration files will be usable through :ref:`command line<generate-conf-file>`
+      or API method :meth:`~fastoad.cmd.api.generate_configuration_file`.
+    - :code:`notebooks`: contains any number of Jupyter notebooks and associated data, that will
+      be made available to users through :ref:`command line<python-usage>`.
+
+Any of these folders is optional. Any other folder will be ignored.
+
+
+Plugin packaging
+################
+
+To make your custom modules usable as a FAST-OAD plugin, you have to package them
+and declare your package as a plugin with :code:`fastoad.plugins` as plugin group name.
+
+Here under is a brief tutorial about these operations using `Poetry <https://python-poetry.org>`_.
+
+.. note::
+
+    If you are not familiar with Python packaging, it is recommended to look at this
+    `tutorial <https://packaging.python.org/en/latest/tutorials/packaging-projects/>`_ first.
+    It presents the important steps and notions of the packaging process, and the "classic" way
+    using `setuptools <https://setuptools.pypa.io/en/latest/>`_.
+    And if you want to stick to setuptools, check this
+    `page <https://packaging.python.org/guides/creating-and-discovering-plugins/#using-package-metadata>`_
+    for details about plugin declaration.
 
-This can be done classically with `setuptools <https://packaging.python.org/guides/creating-and-discovering-plugins/#using-package-metadata>`_.
-It can also be done with `Poetry <https://python-poetry.org>`_, which is the way described below:
 
 .. contents::
    :local:
    :depth: 1
 
-
 ******************************
 Plugin declaration
 ******************************
 
-Assuming you project contains the package :code:`start_trek.drives` that contains
+For the example, let's consider that your project contains the package :code:`star_trek.drives`, and
+that your project structure contains::
+
+    src/
+    ├── star_trek/
+    │   ├── __init__.py
+    │   ├── drives/
+    │   │   ├── __init__.py
+    │   │   ├── configurations/
+    │   │   ├── models/
+    │   │   └── notebooks/
+    │   └── ...
+    └── ...
+
+As previously stated, your folder :code:`src/star_trek/drives` does not have to contain all of the
+folders :code:`models`, :code:`configurations` nor :code:`notebooks`.
+
+Assuming you project contains the package :code:`star_trek.drives` that contains
 models you want to share, you can declare your plugin in your :code:`pyproject.toml`
 file with:
 
@@ -35,10 +114,24 @@ file with:
 
     ...
 
-    [tool.poetry.plugins."fastoad_model"]
-    "internal_models" = "start_trek.drives"
+    [tool.poetry]
+    # Tells location of sources
+    packages = [
+        { include = "star_trek", from = "src" },
+    ]
+
+    ...
+
+    # Plugin declaration
+    [tool.poetry.plugins."fastoad.plugins"]
+    "ST_plugin" = "star_trek.drives"
 
     ...
+
+.. note::
+
+    It is discouraged to declare several FAST-OAD plugins for a same project.
+
 Once your :code:`pyproject.toml` is set, you can do :code:`poetry install`. Besides
 installing your project dependencies, it will make your models **locally** available (i.e.
 you could use their identifiers in your FAST-OAD configuration file without setting
@@ -50,7 +143,7 @@ Building
 ******************************
 You can build your package with the command line :code:`poetry build`.
 Let's assume your :code:`pyproject.toml` file is configured so that your project name is
-:code:`STST_drive_models`, as below:
+:code:`ST_drive_models`, as below:
 
 .. code-block:: toml
 
@@ -60,10 +153,29 @@ Let's assume your :code:`pyproject.toml` file is configured so that your project
     name = "ST_drive_models"
     version = "1.0.0"
 
+    # Tells location of sources
+    packages = [
+        { include = "star_trek", from = "src" },
+    ]
+
     ...
 
-It will create a :code:`dist` folder with two files: :code:`ST_drive_models-1.0.0.tar.gz`
-and :code:`ST_drive_models-1.0.0-py3-none-any.whl` (or something like this).
+    # Specify that Poetry is used for building the package
+    [build-system]
+    requires = ["poetry-core>=1.0.0"]
+    build-backend = "poetry.core.masonry.api"
+
+    ...
+
+    # Plugin declaration
+    [tool.poetry.plugins."fastoad.plugins"]
+    "ST_plugin" = "star_trek.drives"
+    ...
+
+The command :code:`poetry build` will create a :code:`dist` folder with two files:
+
+:code:`ST_drive_models-1.0.0.tar.gz` and :code:`ST_drive_models-1.0.0-py3-none-any.whl`
+(or something like this).
 
 You may then have sent any of those two files to another user, who may then install your models
 using :code:`pip` with:
@@ -79,6 +191,11 @@ Once you have built your package, you may publish it on a a package repository.
 :code:`poetry publish` will publish your package on `PyPI <https://pypi.org>`_,
 provided that you have correctly set your account.
 
+.. note::
+
+    Publishing on PyPI requires a valid account, and also that the chosen package name (defined by
+    `name` field in the `pyproject.toml` file) is unused, or already associated to your account.
+
 Poetry can also publish to another destination.
 
 Please see `here <https://python-poetry.org/docs/cli/#publish>`_ for detailed information.
diff --git a/docs/documentation/usage.rst b/docs/documentation/usage.rst
index f06d4d415..01d8a4484 100644
--- a/docs/documentation/usage.rst
+++ b/docs/documentation/usage.rst
@@ -300,22 +300,47 @@ inline help using
 
     $ fastoad <sub-command> -h
 
+.. _plugin-info:
+
+How to get information about available plugins
+==============================================
+
+FAST-OAD is built on a plugin architecture where each plugin can provide FAST-OAD modules,
+Jupyter notebooks and sample configuration files (see :ref:`plugin addition<add-plugins>`),
+
+A list of installed plugins can be obtained with:
+
+.. code:: shell-session
+
+    $ fastoad plugin_info
 
 .. _generate-conf-file:
 
 How to generate a configuration file
 ====================================
 
-FAST-OAD can provide a ready-to use configuration file with:
+FAST-OAD can provide a ready-to use configuration.
 
 .. code:: shell-session
 
-    $ fastoad gen_conf my_conf.yml
+    $ fastoad gen_conf my_conf.yml --from_package my_plugin_package --source sample_configuration_1.yml
 
-This generates the file `my_conf.yml`
+This copies the file :code:`sample_configuration_1.yml`provided by installed package
+:code:`my_plugin_package` to file :code:`my_conf.yml`.
 
+See :ref:`how to get plugin information<plugin-info>` for listing the values you can put for
+options :code:`--from_package` and :code:`--source`.
 
-.. _get-module-list:
+If only one package is available, option :code:`--from_package` may be omitted.
+If the selected package provides only one configuration file, option :code:`--source` may be omitted.
+
+Hence with FAST-OAD installed (version below 2.0) without additional plugin, the command can be:
+
+.. code:: shell-session
+
+    $ fastoad gen_conf my_conf.yml
+
+.. _`get-module-list`:
 
 How to get list of registered modules
 =====================================
@@ -412,8 +437,8 @@ can provide a `XDSM <https://mdolab.engin.umich.edu/wiki/xdsm-overview>`_.
 
 XDSM offers a more synthetic view than N2 diagram.
 
-As it uses a web service, you need an internet access for this command, but you do not need
-to be a registered user on the WhatsOpt server.
+As it uses a web service, you need an internet access for this command, but you do not need to be
+a registered user on the WhatsOpt server.
 
 You can create a :code:`xdsm.html` file with:
 
@@ -480,4 +505,3 @@ To do so, type this command **in your terminal**:
     $ fastoad notebooks
 
 Then run the Jupyter server as indicated in the obtained message.
-
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 80b70f888..13f9d95c5 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,14 +1,14 @@
 aenum==3.1.0
 alabaster==0.7.12; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
 anyio==3.3.3; python_full_version >= "3.6.2" and python_version >= "3.6"
-appnope==0.1.2; platform_system == "Darwin" and python_version >= "3.7" and sys_platform == "darwin"
+appnope==0.1.2; platform_system == "Darwin" and python_version >= "3.7" and sys_platform == "darwin" and python_full_version >= "3.6.2"
 argcomplete==1.12.3; python_version < "3.8.0" and python_version >= "3.7"
 argon2-cffi==21.1.0; python_version >= "3.6"
 astroid==2.8.2; python_version >= "3.6" and python_version < "4.0"
 atomicwrites==1.4.0; python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.4.0"
 attrs==21.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
 babel==2.9.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-backcall==0.2.0; python_version >= "3.7"
+backcall==0.2.0; python_full_version >= "3.6.2" and python_version >= "3.7"
 backports.entry-points-selectable==1.1.0; python_version >= "2.7" and python_full_version >= "3.6.1"
 black==21.9b0; python_full_version >= "3.6.2"
 bleach==4.1.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
@@ -17,12 +17,12 @@ cffi==1.15.0; implementation_name == "pypy" and python_version >= "3.6"
 cfgv==3.3.1; python_full_version >= "3.6.1"
 charset-normalizer==2.0.7; python_full_version >= "3.6.0" and python_version >= "3.6"
 click==8.0.3; python_version >= "3.6"
-colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" and sys_platform == "win32" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.5.0" and sys_platform == "win32" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6")
+colorama==0.4.4; platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.6.2" and sys_platform == "win32" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.6" and python_full_version >= "3.5.0") and (python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") and python_full_version >= "3.5.0")
 coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4")
 cycler==0.10.0; python_version >= "3.7"
 debugpy==1.5.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
-decorator==5.1.0; python_version >= "3.7"
-defusedxml==0.7.1; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5"
+decorator==5.1.0; python_full_version >= "3.6.2" and python_version >= "3.7"
+defusedxml==0.7.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
 deprecated==1.2.13; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
 distlib==0.3.3; python_full_version >= "3.6.1"
 docutils==0.17.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
@@ -33,7 +33,7 @@ flake8==4.0.1; python_version >= "3.6"
 identify==2.3.0; python_full_version >= "3.6.1"
 idna==3.3; python_full_version >= "3.6.2" and python_version >= "3.6"
 imagesize==1.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-importlib-metadata==4.2.0; python_version == "3.7" and python_full_version >= "3.6.1"
+importlib-metadata==4.2.0; python_version == "3.7" and python_full_version >= "3.6.2" or python_version < "3.10"
 iniconfig==1.1.1; python_version >= "3.6"
 ipopo==1.0.0
 ipykernel==6.4.1; python_version >= "3.7"
@@ -42,25 +42,25 @@ ipython-genutils==0.2.0; python_version >= "3.7"
 ipython==7.28.0; python_full_version >= "3.6.2" and python_version >= "3.7"
 ipywidgets==7.5.0
 isort==5.9.3; python_full_version >= "3.6.1" and python_version < "4.0" and python_version >= "3.6"
-jedi==0.18.0; python_version >= "3.7"
+jedi==0.18.0; python_full_version >= "3.6.2" and python_version >= "3.7"
 jinja2==3.0.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
 json5==0.9.6; python_version >= "3.6"
 jsonrpclib-pelix==0.4.3.1
 jsonschema==3.2.0
 jupyter-client==6.1.1; python_version >= "3.5"
-jupyter-core==4.8.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+jupyter-core==4.8.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
 jupyter-server==1.11.1; python_version >= "3.6"
 jupyterlab-server==2.8.2; python_version >= "3.6"
 jupyterlab==3.0.18; python_version >= "3.6"
 kiwisolver==1.3.2; python_version >= "3.7"
 latexcodec==2.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
 lazy-object-proxy==1.6.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.6.0"
-lxml==4.6.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
+lxml==4.6.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0")
 markupsafe==2.0.1; python_version >= "3.6"
-matplotlib-inline==0.1.3; python_version >= "3.7"
+matplotlib-inline==0.1.3; python_full_version >= "3.6.2" and python_version >= "3.7"
 matplotlib==3.4.0; python_version >= "3.7"
 mccabe==0.6.1; python_version >= "3.6" and python_version < "4.0"
-mistune==0.8.4; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5"
+mistune==0.8.4; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
 mypy-extensions==0.4.3; python_full_version >= "3.6.2"
 nbclassic==0.3.2; python_version >= "3.6"
 nbconvert==5.6.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
@@ -74,11 +74,11 @@ openmdao-extensions==1.0.2; python_version >= "3.6"
 openmdao==3.10.0; python_version >= "3.6"
 packaging==21.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
 pandas==1.2.1; python_full_version >= "3.7.1"
-pandocfilters==1.5.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5"
-parso==0.8.2; python_version >= "3.7"
+pandocfilters==1.5.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
+parso==0.8.2; python_full_version >= "3.6.2" and python_version >= "3.7"
 pathspec==0.9.0; python_full_version >= "3.6.2"
-pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.7"
-pickleshare==0.7.5; python_version >= "3.7"
+pexpect==4.8.0; sys_platform != "win32" and python_version >= "3.7" and python_full_version >= "3.6.2"
+pickleshare==0.7.5; python_full_version >= "3.6.2" and python_version >= "3.7"
 pillow==8.4.0; python_version >= "3.7"
 platformdirs==2.4.0; python_version >= "3.6" and python_full_version >= "3.6.2" and python_version < "4.0"
 plotly==5.0.0; python_version >= "3.6"
@@ -86,7 +86,7 @@ pluggy==1.0.0; python_version >= "3.6"
 pre-commit==2.15.0; python_full_version >= "3.6.1"
 prometheus-client==0.11.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
 prompt-toolkit==3.0.20; python_full_version >= "3.6.2" and python_version >= "3.7"
-ptyprocess==0.7.0; os_name != "nt" and python_version >= "3.7" and sys_platform != "win32"
+ptyprocess==0.7.0; os_name != "nt" and python_version >= "3.7" and sys_platform != "win32" and python_full_version >= "3.6.2"
 py==1.10.0; python_version >= "3.6" and python_full_version < "3.0.0" and implementation_name == "pypy" or python_full_version >= "3.4.0" and python_version >= "3.6" and implementation_name == "pypy"
 pybtex-docutils==1.0.1; python_version >= "3.6"
 pybtex==0.24.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6"
@@ -94,18 +94,18 @@ pycodestyle==2.8.0; python_version >= "3.6" and python_full_version < "3.0.0" or
 pycparser==2.20; python_version >= "3.6" and python_full_version < "3.0.0" and implementation_name == "pypy" or implementation_name == "pypy" and python_version >= "3.6" and python_full_version >= "3.4.0"
 pydoe2==1.3.0; python_version >= "3.6"
 pyflakes==2.4.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
-pygments==2.10.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
+pygments==2.10.0; python_full_version >= "3.6.2" and python_version >= "3.7" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6")
 pylint==2.11.1; python_version >= "3.6" and python_version < "4.0"
 pyparsing==2.4.7; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7"
 pyrsistent==0.18.0; python_version >= "3.6"
 pytest-cov==3.0.0; python_version >= "3.6"
 pytest==6.2.5; python_version >= "3.6"
-python-dateutil==2.8.2; python_full_version >= "3.7.1" and python_version >= "3.5" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5")
+python-dateutil==2.8.2; python_full_version >= "3.7.1" and python_version >= "3.5" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7")
 pytz==2021.3; python_full_version >= "3.7.1" and python_version >= "3.6"
 pywin32==302; sys_platform == "win32" and platform_python_implementation != "PyPy" and python_version >= "3.6"
 pywinpty==1.1.4; os_name == "nt" and python_version >= "3.6"
 pyyaml==6.0; python_version >= "3.6" and python_full_version >= "3.6.1" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6")
-pyzmq==22.3.0; python_version >= "3.6"
+pyzmq==22.3.0; python_version >= "3.7"
 regex==2021.10.8; python_full_version >= "3.6.2"
 requests-unixsocket==0.2.0; python_version >= "3.6"
 requests==2.26.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6"
@@ -113,7 +113,7 @@ ruamel.yaml.clib==0.2.6; platform_python_implementation == "CPython" and python_
 ruamel.yaml==0.16.0
 scipy==1.5.4; python_version >= "3.6"
 send2trash==1.8.0; python_version >= "3.6"
-six==1.16.0; python_full_version >= "3.7.1" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7") and (python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.5") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6")
+six==1.16.0; python_full_version >= "3.7.1" and python_version >= "3.6" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.7") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6")
 sniffio==1.2.0; python_full_version >= "3.6.2" and python_version >= "3.6"
 snowballstemmer==2.1.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
 sphinx-rtd-theme==1.0.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
@@ -129,13 +129,13 @@ stdatm==0.1.0; python_version >= "3.7" and python_version < "4.0"
 tabulate==0.8.9
 tenacity==8.0.1; python_version >= "3.6"
 terminado==0.12.1; python_version >= "3.6"
-testpath==0.5.0; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5"
+testpath==0.5.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6"
 tokenize-rt==4.1.0; python_full_version >= "3.6.2"
 toml==0.10.2; python_full_version >= "3.6.1" and python_version >= "3.6" and python_version < "4" and (python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.3.0") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.6") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.3.0" and python_version >= "3.6")
 tomli==1.2.1; python_version >= "3.6" and python_full_version >= "3.6.2"
 tomlkit==0.5.3; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0")
 tornado==6.1; python_version >= "3.7"
-traitlets==5.1.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7"
+traitlets==5.1.0; python_full_version >= "3.6.2" and python_version >= "3.7" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7")
 typed-ast==1.4.3; python_version < "3.8" and python_full_version >= "3.6.2" and implementation_name == "cpython" and python_version >= "3.6"
 typing-extensions==3.10.0.2
 urllib3==1.26.7; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.6"
@@ -147,4 +147,4 @@ widgetsnbextension==3.5.1; python_version >= "3.6"
 wop==1.20.0; python_version >= "3.6"
 wrapt==1.12.1; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4.0" or python_version >= "3.6" and python_version < "4.0" and python_full_version >= "3.4.0"
 xdsmjs==1.0.1; python_version >= "3.6"
-zipp==3.6.0; python_version >= "3.6" and python_version < "3.8"
+zipp==3.6.0; python_version >= "3.7" and python_version < "3.8" and python_full_version >= "3.6.2"
diff --git a/poetry.lock b/poetry.lock
index 172b78d51..d01858003 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -738,7 +738,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
 [[package]]
 name = "lxml"
-version = "4.6.3"
+version = "4.6.5"
 description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
 category = "main"
 optional = false
@@ -1838,7 +1838,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
 [metadata]
 lock-version = "1.1"
 python-versions = "^3.7"
-content-hash = "ea67ff21131a2bc32dd6af694dff26a36cfd2b374f02fa356bbad4fab01bc8c5"
+content-hash = "f0fe476f8faca863555f8408791b8c64be2d094df6126d3eb58d8789ef698bd5"
 
 [metadata.files]
 aenum = [
@@ -2259,42 +2259,66 @@ lazy-object-proxy = [
     {file = "lazy_object_proxy-1.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b"},
 ]
 lxml = [
-    {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"},
-    {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"},
-    {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"},
-    {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"},
-    {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"},
-    {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"},
-    {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"},
-    {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"},
-    {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"},
-    {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"},
-    {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"},
-    {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"},
-    {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"},
-    {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"},
-    {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"},
-    {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"},
-    {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"},
-    {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"},
-    {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"},
-    {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"},
-    {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"},
-    {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"},
-    {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"},
-    {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"},
-    {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"},
-    {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"},
-    {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"},
-    {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"},
-    {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"},
-    {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"},
-    {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"},
-    {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"},
-    {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"},
-    {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"},
-    {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"},
-    {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"},
+    {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"},
+    {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"},
+    {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"},
+    {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"},
+    {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"},
+    {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"},
+    {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"},
+    {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"},
+    {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"},
+    {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"},
+    {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"},
+    {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"},
+    {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"},
+    {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"},
+    {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"},
+    {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"},
+    {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"},
+    {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"},
+    {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"},
+    {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"},
+    {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"},
+    {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"},
+    {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"},
+    {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"},
+    {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"},
+    {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"},
+    {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"},
+    {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"},
+    {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"},
+    {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"},
+    {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"},
+    {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"},
+    {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"},
+    {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"},
+    {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"},
+    {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"},
+    {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"},
+    {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"},
+    {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"},
+    {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"},
+    {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"},
+    {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"},
+    {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"},
+    {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"},
+    {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"},
+    {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"},
+    {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"},
+    {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"},
+    {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"},
+    {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"},
+    {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"},
+    {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"},
+    {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"},
+    {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"},
+    {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"},
+    {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"},
+    {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"},
+    {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"},
+    {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"},
+    {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"},
 ]
 markupsafe = [
     {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
diff --git a/pyproject.toml b/pyproject.toml
index 4cc511956..7c367dbc2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,34 +1,3 @@
-[tool.black]
-line-length = 100
-
-[tool.pytest.ini_options]
-minversion = "6.0"
-addopts = "--cov fastoad --cov-report term-missing --cov-report html --verbose"
-testpaths = ["src", "tests"]
-norecursedirs = ["dist", "build", ".tox"]
-
-[tool.coverage.run]
-branch = true
-source = ["fastoad"]
-omit = ["*/test/*", "*/tests/*"]
-[tool.coverage.paths]
-source = ["src/", "*/site-packages/"]
-[tool.coverage.report]
-# Regexes for lines to exclude from consideration
-exclude_lines = [
-    # Have to re-enable the standard pragma
-    "pragma: no cover",
-    # Don't complain about missing debug-only code:
-    "def __repr__",
-    "if self.debug",
-    # Don't complain if tests don't hit defensive assertion code:
-    "raise AssertionError",
-    "raise NotImplementedError",
-    # Don't complain if non-runnable code isn't run:
-    "if 0:",
-    "if __name__ = = .__main__.:"
-]
-
 [tool.poetry]
 name = "FAST-OAD"
 version = "0.0.0"
@@ -62,6 +31,7 @@ classifiers = [
     "Operating System :: MacOS",
     "Programming Language :: Python :: 3.7",
     "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
     "Topic :: Scientific/Engineering :: Physics"
 ]
 
@@ -82,7 +52,7 @@ ipywidgets = "^7.5.0"
 ipysheet = ">=0.5.0, <1"
 plotly = "^5.0.0"
 wop = "^1.20.0"
-lxml = "^4.6.3"
+lxml = "^4.6.5"
 tomlkit = ">=0.5.3, <1"
 "ruamel.yaml" = ">=0.15.78, <0.18"
 jsonschema = ">=3.2.0, <5"
@@ -95,6 +65,7 @@ notebook = "^6.0"
 stdatm = "^0.1.0"
 Deprecated = "^1.2.13"
 click = "^8.0.3"
+importlib-metadata = { version = "^4.2", python = "<3.10" }
 
 [tool.poetry.dev-dependencies]
 pytest = "^6.2"
@@ -114,9 +85,40 @@ matplotlib = "^3.1.2"
 fastoad = "fastoad.cmd.cli:fast_oad"
 fast-oad = "fastoad.cmd.cli:fast_oad"
 
-[tool.poetry.plugins."fastoad_model"]
-"internal_models" = "fastoad.models"
+[tool.poetry.plugins."fastoad.plugins"]
+"bundled" = "fastoad"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
 build-backend = "poetry.core.masonry.api"
+
+[tool.black]
+line-length = 100
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = "--cov fastoad --cov-report term-missing --cov-report html --verbose"
+testpaths = ["src", "tests"]
+norecursedirs = ["dist", "build", ".tox"]
+
+[tool.coverage.run]
+branch = true
+source = ["fastoad"]
+omit = ["*/test/*", "*/tests/*"]
+[tool.coverage.paths]
+source = ["src/", "*/site-packages/"]
+[tool.coverage.report]
+# Regexes for lines to exclude from consideration
+exclude_lines = [
+    # Have to re-enable the standard pragma
+    "pragma: no cover",
+    # Don't complain about missing debug-only code:
+    "def __repr__",
+    "if self.debug",
+    # Don't complain if tests don't hit defensive assertion code:
+    "raise AssertionError",
+    "raise NotImplementedError",
+    # Don't complain if non-runnable code isn't run:
+    "if 0:",
+    "if __name__ = = .__main__.:"
+]
diff --git a/src/fastoad/__init__.py b/src/fastoad/__init__.py
index 4ae6892d4..f571f7b39 100644
--- a/src/fastoad/__init__.py
+++ b/src/fastoad/__init__.py
@@ -1,5 +1,5 @@
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -12,17 +12,18 @@
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 
-from pkg_resources import DistributionNotFound, get_distribution
+import sys
 
-import fastoad.module_management._plugins
+if sys.version_info < (3, 10):
+    from importlib_metadata import distribution, PackageNotFoundError
+else:
+    from importlib.metadata import distribution, PackageNotFoundError
 
 try:
     # Change here if project is renamed and does not equal the package name
     dist_name = "FAST-OAD"
-    __version__ = get_distribution(dist_name).version
-except DistributionNotFound:
+    __version__ = distribution(dist_name).version
+except PackageNotFoundError:
     __version__ = "unknown"
 finally:
-    del get_distribution, DistributionNotFound
-
-fastoad.module_management._plugins.load_plugins()
+    del distribution
diff --git a/src/fastoad/_utils/resource_management/contents.py b/src/fastoad/_utils/resource_management/contents.py
new file mode 100644
index 000000000..88f59ed6c
--- /dev/null
+++ b/src/fastoad/_utils/resource_management/contents.py
@@ -0,0 +1,73 @@
+"""Checks about packages and resources"""
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+from importlib.resources import contents
+from typing import List
+
+
+class PackageReader:
+    """
+    Wrapper of `importlib.resources.contents` which handles when `package_name` is
+    not a package or does not exist.
+
+    :param package_name: Name of package to inspect.
+    """
+
+    def __init__(self, package_name: str):
+        """
+        :param package_name:
+        """
+        self.is_package = False
+        self.exists = True
+        self.is_module = False
+        self.has_error = False
+        self._contents = []
+        self.package_name = package_name
+
+    @property
+    def package_name(self):
+        """Name of package to inspect."""
+        return self._package_name
+
+    @package_name.setter
+    def package_name(self, package_name):
+        self._package_name = package_name
+        if package_name:
+            try:
+                self._contents = list(contents(package_name))
+                self.is_package = True
+            except TypeError:
+                self.is_module = True
+            except ModuleNotFoundError:
+                # Either the indicated package does not exist, or it is a file with no extension.
+                # We want to ensure existence to correctly set self.exists.
+                if "." in package_name:
+                    parent_package_name, module_name = package_name.rsplit(".", 1)
+                    parent_package = PackageReader(parent_package_name)
+                    if parent_package.is_package and module_name in parent_package.contents:
+                        self.exists = True
+                else:
+                    # Here we assume non-existence
+                    self.exists = False
+            except Exception:  # pylint: disable = W0703
+                # Here we catch any Python error that may happen when reading the loaded code.
+                # Thus, we ensure to not break the application if a module is incorrectly written.
+                self.has_error = True
+
+    @property
+    def contents(self) -> List[str]:
+        """
+        An iterator if package exists, otherwise an empty list.
+        """
+        return self._contents
diff --git a/src/fastoad/_utils/resource_management/copy.py b/src/fastoad/_utils/resource_management/copy.py
index 780e8454c..52e5ea23b 100644
--- a/src/fastoad/_utils/resource_management/copy.py
+++ b/src/fastoad/_utils/resource_management/copy.py
@@ -2,7 +2,7 @@
 Helper module for copying resources
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -16,11 +16,12 @@
 
 import os.path as pth
 import shutil
-from importlib.resources import Package, contents, is_resource, path
+from importlib.resources import Package, is_resource, path
 from types import ModuleType
 from typing import List
 
-from fastoad._utils.files import make_parent_dir
+from .contents import PackageReader
+from ..files import make_parent_dir
 
 
 def copy_resource(package: Package, resource: str, target_path):
@@ -36,9 +37,6 @@ def copy_resource(package: Package, resource: str, target_path):
     """
     make_parent_dir(target_path)
 
-    if pth.isdir(target_path):
-        target_path = pth.join(target_path, resource)
-
     with path(package, resource) as source_path:
         shutil.copy(source_path, target_path)
 
@@ -64,20 +62,22 @@ def copy_resource_folder(package: Package, destination_path: str, exclude: List[
     if exclude:
         exclusion_list += exclude
 
-    for resource_name in contents(package):
-        if resource_name in exclusion_list:
-            continue
-        if is_resource(package, resource_name):
-            destination_file_path = pth.join(destination_path, resource_name)
-            copy_resource(package, resource_name, destination_file_path)
-        else:
-            # In case of subfolders that are only declared in MANIFEST.in,
-            # getattr(package, "resource_name") will fail (is there another way?).
-            # So we fall back to using package name as as string.
-            if isinstance(package, ModuleType):
-                package_name = package.__name__
-            else:  # str
-                package_name = package
-            new_package_name = ".".join([package_name, resource_name])
-            new_destination_path = pth.join(destination_path, resource_name)
-            copy_resource_folder(new_package_name, new_destination_path, exclude=exclude)
+    package_contents = PackageReader(package).contents
+    if package_contents:
+        for resource_name in package_contents:
+            if resource_name in exclusion_list:
+                continue
+            if is_resource(package, resource_name):
+                destination_file_path = pth.join(destination_path, resource_name)
+                copy_resource(package, resource_name, destination_file_path)
+            else:
+                # In case of subfolders that are only declared in MANIFEST.in,
+                # getattr(package, "resource_name") will fail (is there another way?).
+                # So we fall back to using package name as as string.
+                if isinstance(package, ModuleType):
+                    package_name = package.__name__
+                else:  # str
+                    package_name = package
+                new_package_name = ".".join([package_name, resource_name])
+                new_destination_path = pth.join(destination_path, resource_name)
+                copy_resource_folder(new_package_name, new_destination_path, exclude=exclude)
diff --git a/src/fastoad/api.py b/src/fastoad/api.py
index 2aeb52433..ba57df994 100644
--- a/src/fastoad/api.py
+++ b/src/fastoad/api.py
@@ -1,6 +1,6 @@
 """This module gathers key FAST-OAD classes and functions for convenient import."""
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -17,6 +17,8 @@
 
 
 from fastoad.cmd.api import (
+    get_plugin_information,
+    generate_notebooks,
     evaluate_problem,
     generate_configuration_file,
     generate_inputs,
diff --git a/src/fastoad/cmd/api.py b/src/fastoad/cmd/api.py
index 7db267f6e..625e447a6 100644
--- a/src/fastoad/cmd/api.py
+++ b/src/fastoad/cmd/api.py
@@ -2,7 +2,7 @@
 API
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -15,64 +15,165 @@
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import logging
+import os
 import os.path as pth
+import shutil
 import sys
 import textwrap as tw
-from collections import Iterable
+from collections import defaultdict
+from collections.abc import Iterable
 from time import time
-from typing import IO, List, Union
+from typing import Dict, IO, List, Union
 
 import openmdao.api as om
+import pandas as pd
 from IPython import InteractiveShell
 from IPython.display import HTML, clear_output, display
 from tabulate import tabulate
 
 import fastoad.openmdao.whatsopt
 from fastoad._utils.files import make_parent_dir
-from fastoad._utils.resource_management.copy import copy_resource
-from fastoad.cmd.exceptions import FastFileExistsError
+from fastoad._utils.resource_management.copy import copy_resource, copy_resource_folder
+from fastoad.cmd.exceptions import (
+    FastNoAvailableNotebookError,
+    FastPathExistsError,
+)
 from fastoad.gui import OptimizationViewer, VariableViewer
 from fastoad.io import IVariableIOFormatter
 from fastoad.io.configuration import FASTOADProblemConfigurator
 from fastoad.io.variable_io import DataFile
 from fastoad.io.xml import VariableLegacy1XmlFormatter
 from fastoad.module_management._bundle_loader import BundleLoader
+from fastoad.module_management._plugins import DistributionPluginDefinition, FastoadLoader
 from fastoad.module_management.service_registry import RegisterOpenMDAOSystem, RegisterPropulsion
 from fastoad.openmdao.problem import FASTOADProblem
 from fastoad.openmdao.variables import VariableList
-from . import resources
 
 DEFAULT_WOP_URL = "https://ether.onera.fr/whatsopt"
 _LOGGER = logging.getLogger(__name__)
 
-SAMPLE_FILENAME = "fastoad.yml"
-MAX_TABLE_WIDTH = 200  # For variable list text output
-
 # Used for test purposes only
 _PROBLEM_CONFIGURATOR = None
 
 
-def generate_configuration_file(configuration_file_path: str, overwrite: bool = False):
+def get_plugin_information(print_data=False) -> Dict[str, DistributionPluginDefinition]:
+    """
+    Provides information about available FAST-OAD plugins.
+
+    :param print_data: if True, plugin data are displayed.
+    :return: a dict with installed package names as keys and matching FAST-OAD
+             plugin definitions as values.
+    """
+    plugin_definitions = FastoadLoader().distribution_plugin_definitions
+    if print_data:
+        table_dicts = [plugin_def.to_dict() for plugin_def in plugin_definitions.values()]
+
+        if table_dicts:
+            table = pd.DataFrame(table_dicts)
+            if InteractiveShell.initialized():
+                display(HTML(table.to_html(index=False)))
+            else:
+                print(table.to_markdown(index=False))
+        else:
+            print("No available FAST-OAD plugin.")
+
+    return plugin_definitions
+
+
+def generate_notebooks(
+    destination_path: str,
+    overwrite: bool = False,
+    distribution_name=None,
+):
+    """
+    Copies notebook folder(s) from available plugin(s).
+
+    :param destination_path: the inner structure of the folders will depend on
+                             the number of installed package and the number of
+                             plugins they contain.
+    :param overwrite: if True and `destination_path` exists, it will be removed before writing.
+    :param distribution_name: the name of an installed package that provides notebooks
     """
-    Generates a sample configuration file.
+    # Check available notebooks
+    folder_info_list = FastoadLoader().get_notebook_folder_list(distribution_name)
+
+    if len(folder_info_list) == 0:
+        raise FastNoAvailableNotebookError(distribution_name)
+
+    # Create and copy folder
+    destination_path = pth.abspath(destination_path)
+    if pth.exists(destination_path):
+        if overwrite:
+            shutil.rmtree(destination_path)
+        else:
+            raise FastPathExistsError(
+                f"Notebook folder {destination_path} not written because it already exists. "
+                "Use overwrite=True to bypass.",
+                destination_path,
+            )
+
+    # Write notebooks
+
+    # We organize folder_info_list as dict of dict to easily count how many distributions
+    # and plugin by distributions we have, which info is used for building target paths.
+    folder_info_dict = defaultdict(dict)
+    for folder_info in folder_info_list:
+        folder_info_dict[folder_info.dist_name][folder_info.plugin_name] = folder_info
+
+    only_one_dist = len(folder_info_dict) == 1
+    for dist_content in folder_info_dict.values():
+        only_one_plugin = len(dist_content) == 1
+        for folder_info in dist_content.values():
+            target_path_elements = [destination_path]
+            if not only_one_dist:
+                target_path_elements.append(folder_info.dist_name)
+            if not only_one_plugin:
+                target_path_elements.append(folder_info.plugin_name)
+
+            target_path = pth.join(*target_path_elements)
+            os.makedirs(target_path)
+            copy_resource_folder(folder_info.package_name, target_path)
+
+    return destination_path
+
+
+def generate_configuration_file(
+    configuration_file_path: str,
+    overwrite: bool = False,
+    distribution_name=None,
+    sample_file_name=None,
+):
+    """
+    Copies a sample configuration file from an available plugin.
 
     :param configuration_file_path: the path of file to be written
     :param overwrite: if True, the file will be written, even if it already exists
+    :param distribution_name: the name of the installed package that provides the sample
+                             configuration file (can be omitted if only one plugin is available)
+    :param sample_file_name: the name of the sample configuration file (can be omitted if
+                             the plugin provides only one configuration file)
     :return: path of generated file
-    :raise FastFileExistsError: if overwrite==False and configuration_file_path already exists
+    :raise FastPathExistsError: if overwrite==False and configuration_file_path already exists
     """
+
+    # Check on plugins
+    dist_definition = FastoadLoader().get_distribution_plugin_definition(distribution_name)
+    file_info = dist_definition.get_configuration_file_info(sample_file_name)
+
+    # Check on file overwrite
     configuration_file_path = pth.abspath(configuration_file_path)
     if not overwrite and pth.exists(configuration_file_path):
-        raise FastFileExistsError(
-            "Configuration file %s not written because it already exists. "
-            "Use overwrite=True to bypass." % configuration_file_path,
+        raise FastPathExistsError(
+            f"Configuration file {configuration_file_path} not written because it already exists. "
+            "Use overwrite=True to bypass.",
             configuration_file_path,
         )
 
+    # Write file
     make_parent_dir(configuration_file_path)
+    copy_resource(file_info.package_name, file_info.name, configuration_file_path)
+    _LOGGER.info('Sample configuration written in "%s".', configuration_file_path)
 
-    copy_resource(resources, SAMPLE_FILENAME, configuration_file_path)
-    _LOGGER.info("Sample configuration written in %s", configuration_file_path)
     return configuration_file_path
 
 
@@ -90,16 +191,16 @@ def generate_inputs(
     :param source_path_schema: set to 'legacy' if the source file come from legacy FAST
     :param overwrite: if True, file will be written even if one already exists
     :return: path of generated file
-    :raise FastFileExistsError: if overwrite==False and configuration_file_path already exists
+    :raise FastPathExistsError: if overwrite==False and configuration_file_path already exists
     """
     conf = FASTOADProblemConfigurator(configuration_file_path)
     conf._set_configuration_modifier(_PROBLEM_CONFIGURATOR)
 
     input_file_path = conf.input_file_path
     if not overwrite and pth.exists(conf.input_file_path):
-        raise FastFileExistsError(
-            "Input file %s not written because it already exists. "
-            "Use overwrite=True to bypass." % input_file_path,
+        raise FastPathExistsError(
+            f"Input file {input_file_path} not written because it already exists. "
+            "Use overwrite=True to bypass.",
             input_file_path,
         )
 
@@ -138,7 +239,7 @@ def list_variables(
                      to the tabulate package. See tabulate.tabulate_formats for a complete list.
                      If "var_desc" the file will use the variable_descriptions.txt format.
     :return: path of generated file, or None if no file was generated.
-    :raise FastFileExistsError: if `overwrite==False` and `out` is a file path and the file exists
+    :raise FastPathExistsError: if `overwrite==False` and `out` is a file path and the file exists
     """
     if out is None:
         out = sys.stdout
@@ -168,13 +269,13 @@ def list_variables(
     if isinstance(out, str):
         out = pth.abspath(out)
         if not overwrite and pth.exists(out):
-            raise FastFileExistsError(
-                "File %s not written because it already exists. "
-                "Use overwrite=True to bypass." % out,
+            raise FastPathExistsError(
+                f"File {out} not written because it already exists. "
+                "Use overwrite=True to bypass.",
                 out,
             )
         make_parent_dir(out)
-        out_file = open(out, "w")
+        out_file = open(out, "w", encoding="utf-8")
     else:
         if out == sys.stdout and InteractiveShell.initialized() and not force_text_output:
             display(HTML(variables_df.to_html(index=False)))
@@ -242,7 +343,7 @@ def list_modules(
                               interactive IPython shell (Jupyter notebook). Has no effect in other
                               shells or if out parameter is not sys.stdout
     :return: path of generated file, or None if no file was generated.
-    :raise FastFileExistsError: if `overwrite==False` and `out` is a file path and the file exists
+    :raise FastPathExistsError: if `overwrite==False` and `out` is a file path and the file exists
     """
     if out is None:
         out = sys.stdout
@@ -256,7 +357,7 @@ def list_modules(
         elif pth.isdir(source_path):
             RegisterOpenMDAOSystem.explore_folder(source_path)
         else:
-            raise FileNotFoundError("Could not find %s" % source_path)
+            raise FileNotFoundError(f"Could not find {source_path}")
     elif isinstance(source_path, Iterable):
         for folder_path in source_path:
             if not pth.isdir(folder_path):
@@ -274,14 +375,14 @@ def list_modules(
     if isinstance(out, str):
         out = pth.abspath(out)
         if not overwrite and pth.exists(out):
-            raise FastFileExistsError(
-                "File %s not written because it already exists. "
-                "Use overwrite=True to bypass." % out,
+            raise FastPathExistsError(
+                f"File {out} not written because it already exists. "
+                "Use overwrite=True to bypass.",
                 out,
             )
 
         make_parent_dir(out)
-        out_file = open(out, "w")
+        out_file = open(out, "w", encoding="utf-8")
     else:
         if (
             out == sys.stdout
@@ -334,8 +435,10 @@ def _get_detailed_system_list():
         component.options.undeclare("distributed")
 
         cell_content = (
-            "  IDENTIFIER:   %s\nPATH:         %s\nDOMAIN:       %s\nDESCRIPTION:  %s\n"
-            % (identifier, path, domain.value, tw.indent(tw.dedent(description), "    "))
+            f"IDENTIFIER:   {identifier}\n"
+            f"PATH:         {path}\n"
+            f"DOMAIN:       {domain.value}\n"
+            f"DESCRIPTION:  {tw.indent(tw.dedent(description), '    ')}\n"
         )
         if len(list(component.options.items())) > 0:
             cell_content += component.options.to_table(fmt="grid") + "\n"
@@ -350,10 +453,10 @@ def _get_detailed_system_list():
         if description is None:
             description = ""
 
-        cell_content = "  IDENTIFIER:   %s\nPATH:         %s\nDESCRIPTION:  %s\n" % (
-            identifier,
-            path,
-            tw.indent(tw.dedent(description), "    "),
+        cell_content = (
+            f"IDENTIFIER:   {identifier}\n"
+            f"PATH:         {path}\n"
+            f"DESCRIPTION:  {tw.indent(tw.dedent(description), '    ')}\n"
         )
         cell_list.append([cell_content])
     return cell_list
@@ -367,7 +470,7 @@ def write_n2(configuration_file_path: str, n2_file_path: str = None, overwrite:
     :param n2_file_path: if None, will default to `n2.html`
     :param overwrite:
     :return: path of generated file.
-    :raise FastFileExistsError: if overwrite==False and n2_file_path already exists
+    :raise FastPathExistsError: if overwrite==False and n2_file_path already exists
     """
 
     if not n2_file_path:
@@ -375,9 +478,9 @@ def write_n2(configuration_file_path: str, n2_file_path: str = None, overwrite:
     n2_file_path = pth.abspath(n2_file_path)
 
     if not overwrite and pth.exists(n2_file_path):
-        raise FastFileExistsError(
-            "N2-diagram file %s not written because it already exists. "
-            "Use overwrite=True to bypass." % n2_file_path,
+        raise FastPathExistsError(
+            f"N2-diagram file {n2_file_path} not written because it already exists. "
+            "Use overwrite=True to bypass.",
             n2_file_path,
         )
 
@@ -413,14 +516,14 @@ def write_xdsm(
     :param dry_run: if True, will run wop without sending any request to the server. Generated
                     XDSM will be empty. (for test purpose only)
     :return: path of generated file.
-    :raise FastFileExistsError: if overwrite==False and xdsm_file_path already exists
+    :raise FastPathExistsError: if overwrite==False and xdsm_file_path already exists
     """
     if not xdsm_file_path:
         xdsm_file_path = pth.join(pth.dirname(configuration_file_path), "xdsm.html")
     xdsm_file_path = pth.abspath(xdsm_file_path)
 
     if not overwrite and pth.exists(xdsm_file_path):
-        raise FastFileExistsError(
+        raise FastPathExistsError(
             "XDSM-diagram file %s not written because it already exists. "
             "Use overwrite=True to bypass." % xdsm_file_path,
             xdsm_file_path,
@@ -453,7 +556,7 @@ def _run_problem(
     :param auto_scaling: if True, automatic scaling is performed for design variables and
                          constraints
     :return: the OpenMDAO problem after run
-    :raise FastFileExistsError: if overwrite==False and output data file of problem already exists
+    :raise FastPathExistsError: if overwrite==False and output data file of problem already exists
     """
 
     conf = FASTOADProblemConfigurator(configuration_file_path)
@@ -462,9 +565,9 @@ def _run_problem(
 
     outputs_path = pth.normpath(problem.output_file_path)
     if not overwrite and pth.exists(outputs_path):
-        raise FastFileExistsError(
-            "Problem not run because output file %s already exists. "
-            "Use overwrite=True to bypass." % outputs_path,
+        raise FastPathExistsError(
+            f"Problem not run because output file {outputs_path} already exists. "
+            "Use overwrite=True to bypass.",
             outputs_path,
         )
 
@@ -480,9 +583,9 @@ def _run_problem(
 
     problem.write_outputs()
     if problem.optim_failed:
-        _LOGGER.error("Optimization failed after " + str(computation_time) + " seconds")
+        _LOGGER.error("Optimization failed after %s seconds", computation_time)
     else:
-        _LOGGER.info("Computation finished after " + str(computation_time) + " seconds")
+        _LOGGER.info("Computation finished after %s seconds", computation_time)
 
     _LOGGER.info("Problem outputs written in %s", outputs_path)
 
@@ -496,7 +599,7 @@ def evaluate_problem(configuration_file_path: str, overwrite: bool = False) -> F
     :param configuration_file_path: problem definition
     :param overwrite: if True, output file will be overwritten
     :return: the OpenMDAO problem after run
-    :raise FastFileExistsError: if overwrite==False and output data file of problem already exists
+    :raise FastPathExistsError: if overwrite==False and output data file of problem already exists
     """
     return _run_problem(configuration_file_path, overwrite, "run_model")
 
@@ -512,7 +615,7 @@ def optimize_problem(
     :param auto_scaling: if True, automatic scaling is performed for design variables and
                          constraints
     :return: the OpenMDAO problem after run
-    :raise FastFileExistsError: if overwrite==False and output data file of problem already exists
+    :raise FastPathExistsError: if overwrite==False and output data file of problem already exists
     """
     return _run_problem(configuration_file_path, overwrite, "run_driver", auto_scaling=auto_scaling)
 
diff --git a/src/fastoad/cmd/cli.py b/src/fastoad/cmd/cli.py
index 9b7eef5f3..0c074d141 100644
--- a/src/fastoad/cmd/cli.py
+++ b/src/fastoad/cmd/cli.py
@@ -1,6 +1,6 @@
 """Command Line Interface."""
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -12,55 +12,83 @@
 #  You should have received a copy of the GNU General Public License
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-import os
 import os.path as pth
-import shutil
 
 import click
 import tabulate
 
 import fastoad
-from fastoad import api, notebooks
-from fastoad._utils.resource_management.copy import copy_resource_folder
 from fastoad.cmd.cli_utils import (
     manage_overwrite,
     out_file_option,
     overwrite_option,
 )
+from fastoad.cmd.exceptions import FastNoAvailableNotebookError
+from fastoad.module_management.exceptions import (
+    FastNoDistPluginError,
+    FastSeveralConfigurationFilesError,
+    FastSeveralDistPluginsError,
+    FastUnknownConfigurationFileError,
+    FastUnknownDistPluginError,
+)
+from . import api
 
 NOTEBOOK_FOLDER_NAME = "FAST-OAD_notebooks"
 
 
-@click.group(
-    context_settings=dict(
-        help_option_names=["-h", "--help"],
-    )
-)
+@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
 @click.version_option(fastoad.__version__, "-v", "--version")
 def fast_oad():
     """FAST-OAD main program"""
 
 
-def fast_oad_subcommand(func):
-    """Decorator for adding a command as subcommand to `fast_oad`."""
-    return fast_oad.add_command(func)
+@fast_oad.command(name="plugin_info")
+def plugin_info():
+    """Provides list of installed FAST-OAD plugins."""
+    api.get_plugin_information(print_data=True)
 
 
-@fast_oad_subcommand
-@click.command(name="gen_conf")
+@fast_oad.command(name="gen_conf")
 @click.argument("conf_file", nargs=1)
+@click.option(
+    "-p",
+    "--from_package",
+    nargs=1,
+    help="Name of installed package that provides the sample configuration file.",
+)
+@click.option("-s", "--source", nargs=1, help="Name of source configuration file.")
 @overwrite_option
-def gen_conf(conf_file, force):
-    """Generate a sample configuration file with given argument as name."""
-    manage_overwrite(
-        api.generate_configuration_file,
-        configuration_file_path=conf_file,
-        overwrite=force,
-    )
+def gen_conf(conf_file, from_package, source, force):
+    """
+    Generate a sample configuration file with given argument as name.
+
+    Option "--from_package" has to be used if several FAST-OAD plugins are available.
 
+    Option "--source_name" has to be used if the targeted plugin provides several sample
+    configuration files.
 
-@fast_oad_subcommand
-@click.command(name="gen_inputs")
+    Use "fastoad plugin_info" to get information about available plugins and sample
+    configuration files.
+    """
+    try:
+        manage_overwrite(
+            api.generate_configuration_file,
+            configuration_file_path=conf_file,
+            overwrite=force,
+            distribution_name=from_package,
+            sample_file_name=source,
+        )
+    except (
+        FastNoDistPluginError,
+        FastSeveralDistPluginsError,
+        FastUnknownDistPluginError,
+        FastSeveralConfigurationFilesError,
+        FastUnknownConfigurationFileError,
+    ) as exc:
+        click.echo(exc.args[0])
+
+
+@fast_oad.command(name="gen_inputs")
 @click.argument("conf_file", nargs=1)
 @click.argument("source_file", nargs=1, required=False)
 @overwrite_option
@@ -96,8 +124,7 @@ def gen_inputs(conf_file, source_file, force, legacy):
     )
 
 
-@fast_oad_subcommand
-@click.command(name="list_modules")
+@fast_oad.command(name="list_modules")
 @out_file_option
 @click.option("-v", "--verbose", is_flag=True, help="Shows detailed information for each system.")
 @click.argument("source_path", nargs=-1)
@@ -123,8 +150,7 @@ def list_modules(out_file, force, verbose, source_path):
         print("\nDone. Use --verbose (-v) option for detailed information.")
 
 
-@fast_oad_subcommand
-@click.command(name="list_variables")
+@fast_oad.command(name="list_variables")
 @click.argument("conf_file", nargs=1)
 @out_file_option
 @click.option(
@@ -147,8 +173,7 @@ def list_variables(conf_file, out_file, force, table_format):
     )
 
 
-@fast_oad_subcommand
-@click.command(name="n2")
+@fast_oad.command(name="n2")
 @click.argument("conf_file", nargs=1)
 @click.argument("n2_file", nargs=1, default="n2.html", required=False)
 @overwrite_option
@@ -166,8 +191,7 @@ def write_n2(conf_file, n2_file, force):
     )
 
 
-@fast_oad_subcommand
-@click.command(name="xdsm")
+@fast_oad.command(name="xdsm")
 @click.argument("conf_file", nargs=1)
 @click.argument("xdsm_file", nargs=1, default="xdsm.html", required=False)
 @overwrite_option
@@ -192,8 +216,7 @@ def write_xdsm(conf_file, xdsm_file, depth, server, force):
     )
 
 
-@fast_oad_subcommand
-@click.command(name="eval")
+@fast_oad.command(name="eval")
 @click.argument("conf_file", nargs=1)
 @overwrite_option
 def evaluate(conf_file, force):
@@ -206,8 +229,7 @@ def evaluate(conf_file, force):
     )
 
 
-@fast_oad_subcommand
-@click.command(name="optim")
+@fast_oad.command(name="optim")
 @click.argument("conf_file", nargs=1)
 @overwrite_option
 def optimize(conf_file, force):
@@ -220,31 +242,39 @@ def optimize(conf_file, force):
     )
 
 
-@fast_oad_subcommand
-@click.command(name="notebooks")
+@fast_oad.command(name="notebooks")
 @click.argument("path", nargs=1, default=".", required=False)
-def create_notebooks(path):
+@click.option(
+    "-p", "--from_package", nargs=1, help="Will use only notebooks from this installed package."
+)
+def create_notebooks(path, from_package):
     """
     Creates a FAST-OAD_notebooks/ folder with pre-configured Jupyter notebooks.
 
     If PATH is given, FAST-OAD_notebooks/ will be created in that folder.
 
-    Please note that all content of an existing FAST-OAD_notebooks/ will be overwritten.
+
+
+    IMPORTANT: Please note that all content of an existing FAST-OAD_notebooks/ will be overwritten.
     """
-    # Create and copy folder
-    target_path = pth.abspath(pth.join(path, NOTEBOOK_FOLDER_NAME))
-    if pth.exists(target_path):
-        shutil.rmtree(target_path)
-    os.makedirs(target_path)
-
-    copy_resource_folder(notebooks, target_path)
-    # Note: copy_resource_folder(tutorial, target_path) would fail because of IPython imports
-
-    # Give info for running Jupyter
-    print("")
-    print("Notebooks have been created in %s" % target_path)
-    print("You may now run Jupyter with:")
-    print('   jupyter lab "%s"' % target_path)
+
+    root_target_path = pth.abspath(pth.join(path, NOTEBOOK_FOLDER_NAME))
+    try:
+        if manage_overwrite(
+            api.generate_notebooks,
+            destination_path=root_target_path,
+            overwrite=False,
+            distribution_name=from_package,
+        ):
+            # Give info for running Jupyter
+            click.echo("You may now run Jupyter with:")
+            click.echo(f'   jupyter lab "{root_target_path}"')
+    except (
+        FastNoDistPluginError,
+        FastUnknownDistPluginError,
+        FastNoAvailableNotebookError,
+    ) as exc:
+        click.echo(exc.args[0])
 
 
 if __name__ == "__main__":
diff --git a/src/fastoad/cmd/cli_utils.py b/src/fastoad/cmd/cli_utils.py
index 436d61111..62b5d28d1 100644
--- a/src/fastoad/cmd/cli_utils.py
+++ b/src/fastoad/cmd/cli_utils.py
@@ -1,6 +1,6 @@
 """Utility functions for CLI interface."""
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -16,7 +16,7 @@
 
 import click
 
-from fastoad.cmd.exceptions import FastFileExistsError
+from fastoad.cmd.exceptions import FastPathExistsError
 
 
 def overwrite_option(func):
@@ -51,11 +51,12 @@ def manage_overwrite(func: Callable, filename_func: Callable = None, **kwargs):
     """
     Runs `func`, that is expected to write a file, with provided keyword arguments `args`.
 
-    If the run throws FastFileExistsError, a question is displayed and user is
+    If the run throws FastPathExistsError, a question is displayed and user is
     asked for a yes/no answer. If `yes` is given, arg["overwrite"] is set to True
     and `func` is run again.
 
-    :param func: callable that will do the operation
+    :param func: callable that will do the operation and is expected to return
+                 the path of written element.
     :param filename_func: a function that provides the name of written file, given the
                           value returned by func
     :param kwargs: keyword arguments for func
@@ -65,12 +66,12 @@ def manage_overwrite(func: Callable, filename_func: Callable = None, **kwargs):
     try:
         written = _run_func(func, filename_func, **kwargs)
 
-    except FastFileExistsError as exc:
-        if click.confirm(f'File "{exc.args[1]}" already exists. Do you want to overwrite it?'):
+    except FastPathExistsError as exc:
+        if click.confirm(f'"{exc.args[1]}" already exists. Do you want to overwrite it?'):
             kwargs["overwrite"] = True
             written = _run_func(func, filename_func, **kwargs)
         else:
-            click.echo("No file written.")
+            click.echo("Operation cancelled.")
 
     return written
 
@@ -80,7 +81,7 @@ def _run_func(func: Callable, filename_func: Callable = None, **kwargs):
     if result:
         if filename_func:
             result = filename_func(result)
-        click.echo(f'File "{result}" has been written.')
+        click.echo(f'"{result}" has been written.')
         return True
 
     return False
diff --git a/src/fastoad/cmd/exceptions.py b/src/fastoad/cmd/exceptions.py
index 67119b2ea..d3bb4a0aa 100644
--- a/src/fastoad/cmd/exceptions.py
+++ b/src/fastoad/cmd/exceptions.py
@@ -2,7 +2,7 @@
 Exception for cmd package
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -17,9 +17,23 @@
 from fastoad.exceptions import FastError
 
 
-class FastFileExistsError(FastError):
-    """Raised when asked for writing a file that already exists"""
+class FastPathExistsError(FastError):
+    """Raised when asked for writing a file/folder that already exists."""
 
     def __init__(self, *args):
         super().__init__(*args)
         self.file_path = args[1]
+
+
+class FastNoAvailableNotebookError(FastError):
+    """Raised when no notebook is available for creation."""
+
+    def __init__(self, distribution_name=None):
+        msg = "No notebook available "
+        if distribution_name:
+            msg += f'in installed package "{distribution_name}".'
+        else:
+            msg += "in FAST-OAD plugins."
+
+        self.distribution_name = distribution_name
+        super().__init__(msg)
diff --git a/src/fastoad/module_management/tests/data/dummy_plugin/__init__.py b/src/fastoad/configurations/__init__.py
similarity index 100%
rename from src/fastoad/module_management/tests/data/dummy_plugin/__init__.py
rename to src/fastoad/configurations/__init__.py
diff --git a/src/fastoad/configurations/cs25_base.yaml b/src/fastoad/configurations/cs25_base.yaml
new file mode 100644
index 000000000..d5b30cc4b
--- /dev/null
+++ b/src/fastoad/configurations/cs25_base.yaml
@@ -0,0 +1,74 @@
+title: Sample OAD Process
+
+# List of folder paths where user added custom registered OpenMDAO components
+module_folders:
+
+# Input and output files
+input_file: ./problem_inputs.xml
+output_file: ./problem_outputs.xml
+
+# Definition of problem driver assuming the OpenMDAO convention "import openmdao.api as om"
+driver: om.ScipyOptimizeDriver(tol=1e-2, optimizer='COBYLA')
+
+# Definition of OpenMDAO model
+# Although "model" is a mandatory name for the top level of the model, its
+# sub-components can be freely named by user
+model:
+
+  #  Solvers are defined assuming the OpenMDAO convention "import openmdao.api as om"
+  nonlinear_solver: om.NonlinearBlockGS(maxiter=100, atol=1e-2)
+  linear_solver: om.DirectSolver()
+
+
+  # Components can be put in sub-groups
+  subgroup:
+
+    # A group can be set with its own solvers.
+    # Uncomment the 2 next lines for using the performance module with "mission_file_path: ::sizing_mission"
+    #nonlinear_solver: om.NonlinearBlockGS(maxiter=100, atol=1e-2, iprint=0)
+    #linear_solver: om.DirectSolver()
+
+    geometry:
+      # An OpenMDAO component is identified by its "id"
+      id: fastoad.geometry.legacy
+    weight:
+      id: fastoad.weight.legacy
+    mtow:
+      id: fastoad.mass_performances.compute_MTOW
+    hq_tail_sizing:
+      id: fastoad.handling_qualities.tail_sizing
+    hq_static_margin:
+      id: fastoad.handling_qualities.static_margin
+    wing_position:
+      id: fastoad.loop.wing_position
+    aerodynamics_highspeed:
+      id: fastoad.aerodynamics.highspeed.legacy
+    aerodynamics_lowspeed:
+      id: fastoad.aerodynamics.lowspeed.legacy
+    aerodynamics_takeoff:
+      id: fastoad.aerodynamics.takeoff.legacy
+    aerodynamics_landing:
+      id: fastoad.aerodynamics.landing.legacy
+      use_xfoil: false
+  performance:
+    id: fastoad.performances.mission
+    propulsion_id: fastoad.wrapper.propulsion.rubber_engine
+    mission_file_path: ::sizing_breguet
+    # mission_file_path: ::sizing_mission  # Activate solvers in "subgroup" if you use this line.
+    out_file: ./flight_points.csv
+    adjust_fuel: true
+    is_sizing: true
+  wing_area:
+    id: fastoad.loop.wing_area
+
+optimization: # This section is needed only if optimization process is run
+  design_variables:
+    - name: data:geometry:wing:aspect_ratio
+      lower: 9.0
+      upper: 18.0
+  constraints:
+    - name: data:geometry:wing:span
+      upper: 60.0
+  objective:
+    - name: data:mission:sizing:needed_block_fuel
+      scaler: 1.e-4
diff --git a/src/fastoad/exceptions.py b/src/fastoad/exceptions.py
index 185b7d1dc..3130b7446 100644
--- a/src/fastoad/exceptions.py
+++ b/src/fastoad/exceptions.py
@@ -4,7 +4,7 @@
 
 
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -53,3 +53,4 @@ class FastUnexpectedKeywordArgument(FastError):
 
     def __init__(self, bad_keyword):
         super().__init__("Unexpected keyword argument: %s" % bad_keyword)
+        self.bad_keyword = bad_keyword
diff --git a/src/fastoad/module_management/_bundle_loader.py b/src/fastoad/module_management/_bundle_loader.py
index 47312867c..b2b01c692 100644
--- a/src/fastoad/module_management/_bundle_loader.py
+++ b/src/fastoad/module_management/_bundle_loader.py
@@ -2,7 +2,7 @@
 Basis for registering and retrieving services
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -16,7 +16,6 @@
 
 import logging
 import re
-from importlib.resources import contents
 from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
 
 import pelix
@@ -30,6 +29,7 @@
     FastBundleLoaderDuplicateFactoryError,
     FastBundleLoaderUnknownFactoryNameError,
 )
+from .._utils.resource_management.contents import PackageReader
 
 _LOGGER = logging.getLogger(__name__)
 """Logger for this module"""
@@ -342,21 +342,26 @@ def _install_python_package(self, package_name: str) -> Tuple[Set[Bundle], Set[s
         """
         bundles = set()
         failed = set()
-        try:
-            package_contents = list(contents(package_name))
-        except (TypeError, ModuleNotFoundError):
-            if package_name.endswith(".py"):
-                try:
-                    bundle = self.context.install_bundle(package_name[:-3])
-                    bundles.add(bundle)
-                except BundleException:
-                    failed.add(package_name)
-            return bundles, failed
-
-        for item in package_contents:
-            sub_bundles, sub_failed = self._install_python_package(".".join([package_name, item]))
-            bundles.update(sub_bundles)
-            failed.update(sub_failed)
+
+        package = PackageReader(package_name)
+        if package.has_error or not package.exists:
+            failed.add(package_name)
+        elif package.is_package:
+            # It is a package, let's explore it.
+            for item in package.contents:
+                item_package = ".".join([package_name, item])
+                if "." in item:
+                    # A file. Considered only if it is a Python file. Ignored otherwise.
+                    if item.endswith(".py"):
+                        try:
+                            bundle = self.context.install_bundle(item_package[:-3])
+                            bundles.add(bundle)
+                        except BundleException:
+                            failed.add(package_name)
+                else:
+                    sub_bundles, sub_failed = self._install_python_package(item_package)
+                    bundles.update(sub_bundles)
+                    failed.update(sub_failed)
 
         return bundles, failed
 
diff --git a/src/fastoad/module_management/_plugins.py b/src/fastoad/module_management/_plugins.py
index a62c555b2..0bae9973b 100644
--- a/src/fastoad/module_management/_plugins.py
+++ b/src/fastoad/module_management/_plugins.py
@@ -2,7 +2,7 @@
 Plugin system for declaration of FAST-OAD models.
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -15,24 +15,346 @@
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import logging
+import os.path as pth
+import sys
+from dataclasses import dataclass, field
+from enum import Enum
+from typing import Callable, Dict, List
 
-from pkg_resources import iter_entry_points
+import numpy as np
 
 from fastoad.openmdao.variables import Variable
 from ._bundle_loader import BundleLoader
+from .exceptions import (
+    FastNoDistPluginError,
+    FastSeveralConfigurationFilesError,
+    FastSeveralDistPluginsError,
+    FastUnknownConfigurationFileError,
+    FastUnknownDistPluginError,
+)
+from .._utils.resource_management.contents import PackageReader
+
+if sys.version_info < (3, 10):
+    from importlib_metadata import entry_points, EntryPoint
+else:
+    from importlib.metadata import entry_points, EntryPoint
 
 _LOGGER = logging.getLogger(__name__)  # Logger for this module
 
-MODEL_PLUGIN_ID = "fastoad_model"
+OLD_MODEL_PLUGIN_ID = "fastoad_model"
+MODEL_PLUGIN_ID = "fastoad.plugins"
+
+
+class SubPackageNames(Enum):
+    """Enumeration of possible plugin subpackages."""
+
+    MODELS = "models"
+    NOTEBOOKS = "notebooks"
+    CONFIGURATIONS = "configurations"
+
+
+@dataclass
+class ResourceInfo:
+    """Class for storing information about configuration files provided by plugins."""
+
+    name: str
+    dist_name: str
+    plugin_name: str
+    package_name: str
 
 
-def load_plugins():
+@dataclass
+class PluginDefinition:
     """
-    Loads declared plugins.
+    Stores and provides FAST-OAD plugin data.
+    """
+
+    dist_name: str
+    plugin_name: str
+    package_name: str = ""
+    subpackages: Dict[SubPackageNames, str] = field(default_factory=dict)
+
+    def detect_subfolders(self):
+        """
+        Scans plugin folders and populates :attr:`subpackages`.
+        """
+        package = PackageReader(self.package_name)
+        for subpackage_name in SubPackageNames:
+            if subpackage_name.value in package.contents:
+                self.subpackages[subpackage_name] = ".".join(
+                    [self.package_name, subpackage_name.value]
+                )
+
+    def get_configuration_file_list(self) -> List[ResourceInfo]:
+        """
+        :return: List of configuration files that are provided by the distribution.
+        """
+        if SubPackageNames.CONFIGURATIONS in self.subpackages:
+            return [
+                ResourceInfo(
+                    name=file,
+                    dist_name=self.dist_name,
+                    plugin_name=self.plugin_name,
+                    package_name=self.subpackages[SubPackageNames.CONFIGURATIONS],
+                )
+                for file in PackageReader(self.subpackages[SubPackageNames.CONFIGURATIONS]).contents
+                if pth.splitext(file)[1] in [".yml", ".yaml"]
+            ]
+
+        return []
+
+
+@dataclass()
+class DistributionPluginDefinition(dict):
     """
-    for entry_point in iter_entry_points(MODEL_PLUGIN_ID):
-        plugin_name = entry_point.name
-        module_name = entry_point.module_name
-        _LOGGER.info("Loading FAST-OAD plugin %s", plugin_name)
-        BundleLoader().explore_folder(module_name, is_package=True)
-        Variable.read_variable_descriptions(module_name)
+    Stores and provides data for FAST-OAD plugins provided by a Python distribution.
+    """
+
+    dist_name: str = None
+
+    def read_entry_point(self, entry_point: EntryPoint, group: str):
+        """
+        Adds plugin definition from provided entry point to
+
+        Does nothing if entry_point.dist.project_name is not equal to :attr:`dist_name`
+
+        :param entry_point:
+        :param group:
+        """
+        if self.dist_name != entry_point.dist.name:
+            return
+
+        plugin_definition = PluginDefinition(
+            dist_name=self.dist_name,
+            plugin_name=entry_point.name,
+        )
+        plugin_definition.package_name = entry_point.module
+        self[entry_point.name] = plugin_definition
+
+        if group == OLD_MODEL_PLUGIN_ID:
+            self[entry_point.name].subpackages[SubPackageNames.MODELS] = entry_point.module
+
+        if group == MODEL_PLUGIN_ID:
+            self[entry_point.name].detect_subfolders()
+
+    def get_configuration_file_list(self, plugin_name=None) -> List[ResourceInfo]:
+        """
+        :param plugin_name: If provided, only file names provided by the plugin in
+                            the distribution will be returned, or an empty list if
+                            the plugin is not in the distribution.
+        :return:  List of configuration file information that are provided by the distribution.
+        """
+        file_list = []
+        if plugin_name:
+            if plugin_name in self:
+                file_list = self[plugin_name].get_configuration_file_list()
+        else:
+            for plugin in self.values():
+                file_list += plugin.get_configuration_file_list()
+
+        return file_list
+
+    def get_configuration_file_info(self, file_name=None, plugin_name=None) -> ResourceInfo:
+        """
+        :param file_name: can be None if only one configuration file is provided in the
+                          distribution (or in the plugin if `plugin_name` is provided)
+        :param plugin_name:
+        :return: information for specified configuration file name.
+        :raise FastSeveralConfigurationFilesError: if several configuration files are available
+                                                   but `file_name` has not been provided.
+        :raise FastUnknownConfigurationFileError: if the specified configuration file is not
+                                                  available.
+        """
+        conf_file_list = self.get_configuration_file_list(plugin_name)
+        if file_name is None:
+            if len(conf_file_list) > 1:
+                raise FastSeveralConfigurationFilesError(self.dist_name)
+            file_info = conf_file_list[0]
+        else:
+            matching_list = list(filter(lambda item: item.name == file_name, conf_file_list))
+            if len(matching_list) == 0:
+                raise FastUnknownConfigurationFileError(file_name, self.dist_name)
+
+            # Here we implicitly assume that plugin developers will ensure that there will be
+            # no duplicates in conf file names (possible if several plugins are in the
+            # same installed package, but such practice is discouraged).
+            file_info = matching_list[0]
+
+        return file_info
+
+    def get_notebook_folder_list(self, plugin_name=None) -> List[ResourceInfo]:
+        """
+        :param plugin_name: If provided, only notebook folder provided by the plugin (if any)
+                            will be returned, or an empty list if the plugin is not in the
+                            distribution.
+        :return:  List of notebook folder information that are provided by the distribution.
+        """
+        if plugin_name:
+            plugin_names = [plugin_name] if plugin_name in self else []
+        else:
+            plugin_names = self.keys()
+
+        info = []
+        for name in plugin_names:
+            plugin_def = self[name]
+            if SubPackageNames.NOTEBOOKS in plugin_def.subpackages:
+                info.append(
+                    ResourceInfo(
+                        name=SubPackageNames.NOTEBOOKS.value,
+                        dist_name=self.dist_name,
+                        plugin_name=name,
+                        package_name=plugin_def.subpackages[SubPackageNames.NOTEBOOKS],
+                    )
+                )
+        return info
+
+    def to_dict(self) -> Dict:
+        """
+        :return: a dict that contains plugin information
+        """
+        has_models = np.any(
+            [SubPackageNames.MODELS in definition.subpackages for definition in self.values()]
+        )
+        has_notebooks = np.any(
+            [SubPackageNames.NOTEBOOKS in definition.subpackages for definition in self.values()]
+        )
+        conf_files = [item.name for item in self.get_configuration_file_list()]
+        return dict(
+            installed_package=self.dist_name,
+            has_models=has_models,
+            has_notebooks=has_notebooks,
+            configurations=sorted(conf_files),
+        )
+
+
+class FastoadLoader(BundleLoader):
+    """
+    Specialized :class:`BundleLoader` that will load plugins at first instantiation.
+
+    I also provides data from available plugins
+    """
+
+    # This class attribute is private and is accessed through a property to ensure
+    # that the class has been instantiated before the attribute is used.
+    _dist_plugin_definitions: Dict[str, DistributionPluginDefinition]
+
+    _loaded = False
+
+    def __init__(self):
+        super().__init__()
+        if not self.__class__._loaded:
+            # Setting cls.loaded to True already ensures that a second instantiation
+            # during loading will not result in an import cycle.
+            self.__class__._loaded = True
+            self.__class__._dist_plugin_definitions = {}
+            self.read_entry_points()
+            self.load()
+
+    @property
+    def distribution_plugin_definitions(self) -> Dict[str, DistributionPluginDefinition]:
+        """Stores plugin definitions with distribution names as dict keys."""
+        return self._dist_plugin_definitions.copy()
+
+    def get_distribution_plugin_definition(
+        self, dist_name: str = None
+    ) -> DistributionPluginDefinition:
+        """
+        :param dist_name: needed if more than one distribution with FAST-OAD plugin is installed.
+        :return: the DistributionPluginDefinition instance that matches `dist_name`, if any
+        :raise FastNoDistPluginError: if no distribution with plugin is available.
+        :raise FastSeveralDistPluginsError: if several distributions are available but `dist_name`
+                                        has not been provided.
+        :raise FastUnknownDistPluginError: if the specified distribution is not available.
+        """
+
+        if len(self._dist_plugin_definitions) == 0:
+            raise FastNoDistPluginError()
+
+        if dist_name is None:
+            if len(self._dist_plugin_definitions) > 1:
+                raise FastSeveralDistPluginsError()
+            return next(iter(self._dist_plugin_definitions.values()))
+
+        if dist_name not in self._dist_plugin_definitions:
+            raise FastUnknownDistPluginError(dist_name)
+
+        return self._dist_plugin_definitions[dist_name]
+
+    def get_configuration_file_list(self, dist_name: str) -> List[ResourceInfo]:
+        """
+        :param dist_name: the distribution to inspect
+        :return: list of configuration files available for named distribution,
+                 or an empty list if the specified distribution is not available
+        """
+        return self._get_resource_list(
+            DistributionPluginDefinition.get_configuration_file_list,
+            dist_name,
+        )
+
+    def get_notebook_folder_list(self, dist_name: str = None) -> List[ResourceInfo]:
+        """
+        Returns the list of notebook folders available for named distribution
+        and optionally the named plugin of this distribution.
+
+        :param dist_name: the distribution to inspect
+        :return: list of notebook folders available for named distribution,
+                 or an empty list if the specified distribution is not available
+        """
+        return self._get_resource_list(
+            DistributionPluginDefinition.get_notebook_folder_list,
+            dist_name,
+        )
+
+    def _get_resource_list(self, method: Callable, dist_name: str = None) -> List[ResourceInfo]:
+        infos = []
+        if dist_name:
+            if dist_name not in self._dist_plugin_definitions:
+                raise FastUnknownDistPluginError(dist_name)
+            dist_names = [dist_name]
+        else:
+            if len(self._dist_plugin_definitions) == 0:
+                raise FastNoDistPluginError()
+            dist_names = self._dist_plugin_definitions.keys()
+
+        for name in dist_names:
+            dist_plugin_definitions = self._dist_plugin_definitions[name]
+            infos += method(dist_plugin_definitions)
+
+        return infos
+
+    @classmethod
+    def read_entry_points(cls):
+        """
+        Reads definitions of declared plugins.
+        """
+        for group in [OLD_MODEL_PLUGIN_ID, MODEL_PLUGIN_ID]:
+            for entry_point in entry_points(group=group):
+                if entry_point.dist.name not in cls._dist_plugin_definitions:
+                    cls._dist_plugin_definitions[
+                        entry_point.dist.name
+                    ] = DistributionPluginDefinition()
+                plugin_dist = cls._dist_plugin_definitions[entry_point.dist.name]
+                plugin_dist.dist_name = entry_point.dist.name
+                plugin_dist.read_entry_point(entry_point, group)
+
+    @classmethod
+    def load(cls):
+        """
+        Loads declared plugins.
+        """
+        for dist_plugin_definitions in cls._dist_plugin_definitions.values():
+            for plugin_name, plugin_def in dist_plugin_definitions.items():
+                _LOGGER.info("Loading FAST-OAD plugin %s", plugin_name)
+                cls._load_models(plugin_def)
+
+    @classmethod
+    def _load_models(cls, plugin_definition: PluginDefinition):
+        """Loads models from plugin."""
+        if SubPackageNames.MODELS in plugin_definition.subpackages:
+            _LOGGER.debug("   Loading models")
+            BundleLoader().explore_folder(
+                plugin_definition.subpackages[SubPackageNames.MODELS], is_package=True
+            )
+            Variable.read_variable_descriptions(
+                plugin_definition.subpackages[SubPackageNames.MODELS]
+            )
diff --git a/src/fastoad/module_management/exceptions.py b/src/fastoad/module_management/exceptions.py
index 14dcb1563..bee132725 100644
--- a/src/fastoad/module_management/exceptions.py
+++ b/src/fastoad/module_management/exceptions.py
@@ -1,6 +1,6 @@
 """Exceptions for module_management package."""
-#  This file is part of FAST : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -12,7 +12,7 @@
 #  You should have received a copy of the GNU General Public License
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-from typing import Sequence, List
+from typing import List, Sequence
 
 from fastoad.exceptions import FastError
 
@@ -131,3 +131,58 @@ def __init__(self, service_id: str, submodel_id: str, submodel_ids: List[str]):
         super().__init__(msg)
         self.service_id = service_id
         self.submodel_id = submodel_id
+
+
+class FastNoDistPluginError(FastError):
+    """Raised when no installed package with FAST-OAD plugin is available."""
+
+    def __init__(self):
+        super().__init__("This feature needs plugins, but no plugin available.")
+
+
+class FastUnknownDistPluginError(FastError):
+    """Raised when a distribution name is not found among distribution with FAST-OAD plugins."""
+
+    def __init__(self, dist_name):
+        self.dist_name = dist_name
+        super().__init__(
+            f'No installed package with FAST-OAD plugin found with name "{dist_name}".'
+        )
+
+
+class FastSeveralDistPluginsError(FastError):
+    """
+    Raised when no distribution name has been specified but several distribution
+    with FAST-OAD plugins are available.
+    """
+
+    def __init__(self):
+        super().__init__(
+            "Several installed packages with FAST-OAD plugins are available. "
+            "One must be specified."
+        )
+
+
+class FastUnknownConfigurationFileError(FastError):
+    """Raised when a configuration file is not found for named distribution."""
+
+    def __init__(self, configuration_file, dist_name):
+        self.configuration_file = configuration_file
+        self.dist_name = dist_name
+        super().__init__(
+            f'Configuration file "{configuration_file}" not provided with '
+            f'installed package "{dist_name}".'
+        )
+
+
+class FastSeveralConfigurationFilesError(FastError):
+    """
+    Raised when no configuration file has been specified but several configuration files are
+    provided with the distribution."""
+
+    def __init__(self, dist_name):
+        self.dist_name = dist_name
+        super().__init__(
+            f'Installed package "{dist_name}" provides several configuration files. '
+            "One must be specified."
+        )
diff --git a/src/fastoad/module_management/service_registry.py b/src/fastoad/module_management/service_registry.py
index afc684ed0..7d4154f0a 100644
--- a/src/fastoad/module_management/service_registry.py
+++ b/src/fastoad/module_management/service_registry.py
@@ -20,7 +20,7 @@
 import openmdao.api as om
 from openmdao.core.system import System
 
-from ._bundle_loader import BundleLoader
+from ._plugins import FastoadLoader
 from .constants import (
     DESCRIPTION_PROPERTY_NAME,
     DOMAIN_PROPERTY_NAME,
@@ -65,8 +65,6 @@ class RegisterSomeService( RegisterService, base_class=ISomeService):
             "Allows to register classes that implement interface ISomeService."
     """
 
-    _loader = BundleLoader()
-
     @classmethod
     def __init_subclass__(cls, *, base_class: type = object):
         """
@@ -93,7 +91,7 @@ def __call__(self, service_class: Type[T]) -> Type[T]:
                 service_class, self._service_id, self._base_class
             )
 
-        return self._loader.register_factory(
+        return FastoadLoader().register_factory(
             service_class, self._id, self._service_id, self.get_properties(service_class)
         )
 
@@ -119,7 +117,7 @@ def explore_folder(cls, folder_path: str):
 
         :param folder_path:
         """
-        cls._loader.explore_folder(folder_path)
+        FastoadLoader().explore_folder(folder_path)
 
     @classmethod
     def get_provider_ids(cls, service_id: str) -> List[str]:
@@ -127,7 +125,7 @@ def get_provider_ids(cls, service_id: str) -> List[str]:
         :param service_id:
         :return: the list of identifiers of providers of the service.
         """
-        return cls._loader.get_factory_names(service_id)
+        return FastoadLoader().get_factory_names(service_id)
 
     @classmethod
     def get_provider(cls, service_provider_id: str, options: dict = None) -> Any:
@@ -139,13 +137,13 @@ def get_provider(cls, service_provider_id: str, options: dict = None) -> Any:
         :return: the created instance
         """
 
-        properties = cls._loader.get_factory_properties(service_provider_id).copy()
+        properties = FastoadLoader().get_factory_properties(service_provider_id).copy()
 
         if options:
             properties[OPTION_PROPERTY_NAME] = properties[OPTION_PROPERTY_NAME].copy()
             properties[OPTION_PROPERTY_NAME].update(options)
 
-        return cls._loader.instantiate_component(service_provider_id, properties)
+        return FastoadLoader().instantiate_component(service_provider_id, properties)
 
     @classmethod
     def get_provider_description(cls, instance_or_id: Union[str, T]) -> str:
@@ -173,9 +171,9 @@ def _get_provider_property(cls, instance_or_id: Any, property_name: str) -> Any:
         """
 
         if isinstance(instance_or_id, str):
-            return cls._loader.get_factory_property(instance_or_id, property_name)
+            return FastoadLoader().get_factory_property(instance_or_id, property_name)
 
-        return cls._loader.get_instance_property(instance_or_id, property_name)
+        return FastoadLoader().get_instance_property(instance_or_id, property_name)
 
 
 class _RegisterOpenMDAOService(RegisterService, base_class=System):
@@ -468,7 +466,7 @@ def get_submodel(cls, service_id: str, options: dict = None):
         :param options:
         :return: the instantiated submodel
         """
-        submodel_ids = cls._loader.get_factory_names(service_id)
+        submodel_ids = FastoadLoader().get_factory_names(service_id)
 
         if service_id in cls.active_models:
             submodel_id = cls.active_models[service_id]
diff --git a/src/fastoad/openmdao/variables.py b/src/fastoad/openmdao/variables.py
index 010c045a9..dbab6a1e2 100644
--- a/src/fastoad/openmdao/variables.py
+++ b/src/fastoad/openmdao/variables.py
@@ -2,7 +2,7 @@
 Module for managing OpenMDAO variables
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -19,17 +19,17 @@
 import os.path as pth
 from builtins import isinstance
 from copy import deepcopy
-from importlib.resources import contents, open_text
+from importlib.resources import open_text
 from typing import Dict, Hashable, Iterable, List, Mapping, Tuple, Union
 
 import numpy as np
 import openmdao.api as om
 import pandas as pd
 
-from ._utils import get_unconnected_input_names
+from fastoad._utils.resource_management.contents import PackageReader
+from fastoad.openmdao._utils import get_unconnected_input_names
 
-# Logger for this module
-_LOGGER = logging.getLogger(__name__)
+_LOGGER = logging.getLogger(__name__)  # Logger for this module
 
 DESCRIPTION_FILENAME = "variable_descriptions.txt"
 
@@ -170,7 +170,7 @@ def read_variable_descriptions(cls, file_parent: str, update_existing: bool = Tr
                     description_file = open(file_path)
             else:
                 # Then it is a module name
-                if DESCRIPTION_FILENAME in contents(file_parent):
+                if DESCRIPTION_FILENAME in PackageReader(file_parent).contents:
                     description_file = open_text(file_parent, DESCRIPTION_FILENAME)
 
         if description_file is not None:


Test patch:
diff --git a/.PyCharmRunConfs/pytest all no-cov.run.xml b/.PyCharmRunConfs/pytest all no-cov.run.xml
index f047a2bc2..ab8e40bfe 100644
--- a/.PyCharmRunConfs/pytest all no-cov.run.xml
+++ b/.PyCharmRunConfs/pytest all no-cov.run.xml
@@ -1,6 +1,6 @@
 <!--
   ~ This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-  ~ Copyright (C) 2020  ONERA & ISAE-SUPAERO
+  ~ Copyright (C) 2022 ONERA & ISAE-SUPAERO
   ~ FAST is free software: you can redistribute it and/or modify
   ~ it under the terms of the GNU General Public License as published by
   ~ the Free Software Foundation, either version 3 of the License, or
@@ -16,8 +16,8 @@
 <component name="ProjectRunConfigurationManager">
   <configuration default="false" name="pytest all no-cov" type="CompoundRunConfigurationType">
     <toRun name="pytest integration no-cov" type="tests" />
-    <toRun name="pytest units no-cov" type="tests" />
     <toRun name="pytest notebooks no-cov" type="tests" />
+    <toRun name="pytest units no-cov" type="tests" />
     <method v="2" />
   </configuration>
 </component>
\ No newline at end of file
diff --git a/.PyCharmRunConfs/pytest notebooks no-cov.run.xml b/.PyCharmRunConfs/pytest notebooks no-cov.run.xml
index c54a32422..52cb7bcc4 100644
--- a/.PyCharmRunConfs/pytest notebooks no-cov.run.xml
+++ b/.PyCharmRunConfs/pytest notebooks no-cov.run.xml
@@ -1,6 +1,6 @@
 <!--
   ~ This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-  ~ Copyright (C) 2021 ONERA & ISAE-SUPAERO
+  ~ Copyright (C) 2022 ONERA & ISAE-SUPAERO
   ~ FAST is free software: you can redistribute it and/or modify
   ~ it under the terms of the GNU General Public License as published by
   ~ the Free Software Foundation, either version 3 of the License, or
@@ -26,7 +26,7 @@
     <option name="_new_keywords" value="&quot;&quot;" />
     <option name="_new_parameters" value="&quot;&quot;" />
     <option name="_new_additionalArguments" value="&quot;--no-cov --nbval-lax -p no:python&quot;" />
-    <option name="_new_target" value="&quot;fastoad&quot;" />
+    <option name="_new_target" value="&quot;fastoad.notebooks&quot;" />
     <option name="_new_targetType" value="&quot;PYTHON&quot;" />
     <method v="2" />
   </configuration>
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index cb4869cc9..c9795e1c2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -87,5 +87,5 @@ jobs:
 
       - name: Notebook tests
         if: ${{ github.event_name == 'pull_request' || contains(github.event.head_commit.message, '[test nb]') || github.ref == 'refs/heads/master' }}
-        run: poetry run pytest --no-cov --nbval-lax -p no:python src
+        run: poetry run pytest --no-cov --nbval-lax -p no:python src/fastoad/notebooks
         shell: bash
diff --git a/.github/workflows/watchman_tests.yml b/.github/workflows/watchman_tests.yml
index a3ed6d2bb..0529c06af 100644
--- a/.github/workflows/watchman_tests.yml
+++ b/.github/workflows/watchman_tests.yml
@@ -34,7 +34,7 @@ jobs:
         shell: bash
 
       - name: Unit tests
-        run: pytest --no-cov src
+        run: pytest --no-cov src -c ./conftest.py
         shell: bash
 
       - name: Notebook tests
diff --git a/src/conftest.py b/src/conftest.py
new file mode 100644
index 000000000..4ec2cd2ff
--- /dev/null
+++ b/src/conftest.py
@@ -0,0 +1,270 @@
+"""
+    Basic settings for tests
+"""
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# Note: this file has to be put in src/, not in project root folder, to ensure that
+# `pytest src` will run OK after a `pip install .`
+
+import os.path as pth
+import sys
+from platform import system
+from shutil import which
+from typing import List, Optional
+from unittest.mock import Mock
+
+import pytest
+
+if sys.version_info < (3, 10):
+    from importlib_metadata import EntryPoint, distribution, Distribution, EntryPoints
+else:
+    from importlib.metadata import EntryPoint, distribution, Distribution, EntryPoints
+
+from fastoad.module_management._plugins import FastoadLoader, MODEL_PLUGIN_ID
+
+DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
+RESULTS_FOLDER_PATH = pth.join(pth.dirname(__file__), "api", "results")
+CONFIGURATION_FILE_PATH = pth.join(DATA_FOLDER_PATH, "sellar.yml")
+
+
+@pytest.fixture(autouse=True)
+def no_xfoil_skip(request, xfoil_path):
+    """
+    Use @pytest.mark.skip_if_no_xfoil() before a test to skip it if xfoil_path
+    fixture returns None and OS is not Windows.
+    """
+    if request.node.get_closest_marker("skip_if_no_xfoil"):
+        if xfoil_path is None and system() != "Windows":
+            pytest.skip("No XFOIL executable available")
+
+
+@pytest.fixture
+def xfoil_path() -> Optional[str]:
+    """
+    On a system that is not Windows, a XFOIL executable with name "xfoil" can
+    be put in "<project_root>/tests/xfoil_exe/".
+    In this case, this fixture will return its path.
+
+    :return: The path of the XFOIL executable
+    """
+    if system() == "Windows":
+        # On Windows, we use the embedded executable
+        return None
+
+    path = pth.abspath(pth.join("tests/xfoil_exe", "xfoil"))
+    if pth.exists(path):
+        # If there is a local xfoil, use it
+        return path
+
+    # Otherwise, use one that is in PATH, if it exists
+    return which("xfoil")
+
+
+@pytest.fixture
+def plugin_root_path() -> str:
+    """Returns the path to dummy plugins, for check purpose."""
+    return pth.abspath("tests/dummy_plugins")
+
+
+@pytest.fixture
+def with_no_plugin():
+    """
+    Ensures that FAST-OAD has no plugin registered.
+
+    Any previous state of plugins is restored during teardown.
+    """
+    _update_entry_map([])
+    yield
+    _teardown()
+
+
+@pytest.fixture
+def with_dummy_plugin_1():
+    """
+    Reduces plugin list to dummy-dist-1 with plugin test_plugin_1
+    (one configuration file, no models, notebook folder).
+
+    Any previous state of plugins is restored during teardown.
+    """
+    _setup()
+    dummy_dist_1 = Mock(Distribution)
+    dummy_dist_1.name = "dummy-dist-1"
+    entry_points = [
+        EntryPoint(
+            name="test_plugin_1",
+            value="tests.dummy_plugins.dist_1.dummy_plugin_1",
+            group=MODEL_PLUGIN_ID,
+        )
+    ]
+    entry_points[0].dist = dummy_dist_1
+    _update_entry_map(entry_points)
+    yield
+    _teardown()
+
+
+@pytest.fixture
+def with_dummy_plugin_2():
+    """
+    Reduces plugin list to dummy-dist-2 with plugin test_plugin_2
+    (one configuration file, model folder, no notebooks).
+
+    Any previous state of plugins is restored during teardown.
+    """
+    _setup()
+    dummy_dist_2 = Mock(Distribution)
+    dummy_dist_2.name = "dummy-dist-2"
+    entry_points = [
+        EntryPoint(
+            name="test_plugin_2",
+            value="tests.dummy_plugins.dist_2.dummy_plugin_2",
+            group=MODEL_PLUGIN_ID,
+        )
+    ]
+    entry_points[0].dist = dummy_dist_2
+    _update_entry_map(entry_points)
+    yield
+    _teardown()
+
+
+@pytest.fixture
+def with_dummy_plugin_distribution_1():
+    """
+    Reduces plugin list to dummy-dist-1 with plugins test_plugin_1 and test_plugin_4
+    (one configuration file, no models, notebook folder).
+
+    Any previous state of plugins is restored during teardown.
+    """
+    _setup()
+    dummy_dist_1 = Mock(Distribution)
+    dummy_dist_1.name = "dummy-dist-1"
+    entry_points = [
+        EntryPoint(
+            name="test_plugin_1",
+            value="tests.dummy_plugins.dist_1.dummy_plugin_1",
+            group=MODEL_PLUGIN_ID,
+        ),
+        EntryPoint(
+            name="test_plugin_4",
+            value="tests.dummy_plugins.dist_1.dummy_plugin_4",
+            group=MODEL_PLUGIN_ID,
+        ),
+    ]
+
+    entry_points[0].dist = entry_points[1].dist = dummy_dist_1
+
+    _update_entry_map(entry_points)
+    yield
+    _teardown()
+
+
+@pytest.fixture
+def with_dummy_plugins():
+    """
+    Reduces plugin list to:
+        - dummy-dist-1 with plugins test_plugin_1 and test_plugin_4
+          (one configuration file, no models, notebook folder).
+        - dummy-dist-2 with plugins test_plugin_2 and test_plugin_3
+          (3 configuration files, model folder, no notebooks).
+        - dummy-dist-3 with plugins test_plugin_5
+          (no configuration file, model folder, notebook folder).
+
+    Any previous state of plugins is restored during teardown.
+    """
+    _setup()
+    dummy_dist_1 = Mock(Distribution)
+    dummy_dist_1.name = "dummy-dist-1"
+    dummy_dist_2 = Mock(Distribution)
+    dummy_dist_2.name = "dummy-dist-2"
+    dummy_dist_3 = Mock(Distribution)
+    dummy_dist_3.name = "dummy-dist-3"
+    entry_points = [
+        EntryPoint(
+            name="test_plugin_1",
+            value="tests.dummy_plugins.dist_1.dummy_plugin_1",
+            group=MODEL_PLUGIN_ID,
+        ),
+        EntryPoint(
+            name="test_plugin_4",
+            value="tests.dummy_plugins.dist_1.dummy_plugin_4",
+            group=MODEL_PLUGIN_ID,
+        ),
+        EntryPoint(
+            name="test_plugin_2",
+            value="tests.dummy_plugins.dist_2.dummy_plugin_2",
+            group=MODEL_PLUGIN_ID,
+        ),
+        EntryPoint(
+            name="test_plugin_3",
+            value="tests.dummy_plugins.dist_2.dummy_plugin_3",
+            group=MODEL_PLUGIN_ID,
+        ),
+        EntryPoint(
+            name="test_plugin_5",
+            value="tests.dummy_plugins.dist_3.dummy_plugin_5",
+            group=MODEL_PLUGIN_ID,
+        ),
+    ]
+    entry_points[0].dist = entry_points[1].dist = dummy_dist_1
+    entry_points[2].dist = entry_points[3].dist = dummy_dist_2
+    entry_points[4].dist = dummy_dist_3
+
+    _update_entry_map(entry_points)
+    yield
+    _teardown()
+
+
+ORIGINAL_ENTRY_POINTS_PROPERTY = Distribution.entry_points
+try:
+    ORIGINAL_ENTRY_POINT_SETATTR = Distribution.__setattr__
+except AttributeError:
+    ORIGINAL_ENTRY_POINT_SETATTR = None
+
+
+def _update_entry_map(new_plugin_entry_points: List[EntryPoint]):
+    """
+    Modified plugin entry_points of FAST-OAD distribution.
+
+    This is done by replacing the entry_points property of Distribution class
+
+    :param new_plugin_entry_points:
+    """
+    dist: Distribution = distribution("FAST-OAD")
+
+    entry_points = EntryPoints(
+        [ep for ep in dist.entry_points if ep.group != MODEL_PLUGIN_ID] + new_plugin_entry_points
+    )
+
+    # Distribution.entry_points gets info directly from entry_points.txt.
+    # Therefore, the only way to modify the output of Distribution.entry_points
+    # is to replace the property.
+    setattr(Distribution, "entry_points", property(lambda self: entry_points))
+
+    # Ensure next instantiation of FastoadLoader will trigger reloading plugins
+    FastoadLoader._loaded = False
+
+
+def _setup():
+    # In last versions of importlib-metadata, EntryPoint overloads __setattr__ to
+    # prevent any attribute modification, but it does not suit our needs.
+    try:
+        delattr(EntryPoint, "__setattr__")
+    except AttributeError:
+        pass
+
+
+def _teardown():
+    if ORIGINAL_ENTRY_POINT_SETATTR:
+        setattr(EntryPoint, "__setattr__", ORIGINAL_ENTRY_POINT_SETATTR)
+    setattr(Distribution, "entry_points", ORIGINAL_ENTRY_POINTS_PROPERTY)
+    FastoadLoader._loaded = False
diff --git a/src/fastoad/_utils/resource_management/tests/test_contents.py b/src/fastoad/_utils/resource_management/tests/test_contents.py
new file mode 100644
index 000000000..64bd09d35
--- /dev/null
+++ b/src/fastoad/_utils/resource_management/tests/test_contents.py
@@ -0,0 +1,55 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+import os.path as pth
+
+from ..contents import PackageReader
+
+
+def test_get_package_contents():
+    # Normal package
+    reader = PackageReader(__package__)
+    assert reader.is_package
+    assert not reader.is_module
+    assert not reader.has_error
+
+    assert pth.basename(__file__) in reader.contents
+    assert "__init__.py" in reader.contents
+    assert "resources" in reader.contents
+
+    # File
+    reader = PackageReader(__name__)
+    assert not reader.is_package
+    assert reader.is_module
+    assert not reader.has_error
+    assert reader.contents == []
+
+    # Bad package name
+    reader = PackageReader(__package__ + ".bad")
+    assert not reader.is_package
+    assert not reader.is_module
+    assert not reader.has_error
+    assert reader.contents == []
+
+    # Empty name
+    reader = PackageReader("")
+    assert not reader.is_package
+    assert not reader.is_module
+    assert not reader.has_error
+    assert reader.contents == []
+
+    reader = PackageReader(None)
+    assert not reader.is_package
+    assert not reader.is_module
+    assert not reader.has_error
+    assert reader.contents == []
diff --git a/tests/testing_utilities.py b/src/fastoad/_utils/testing.py
similarity index 89%
rename from tests/testing_utilities.py
rename to src/fastoad/_utils/testing.py
index 7709c223c..b56d5027d 100644
--- a/tests/testing_utilities.py
+++ b/src/fastoad/_utils/testing.py
@@ -2,7 +2,7 @@
 Convenience functions for helping tests
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -37,8 +37,8 @@ def run_system(
         model.linear_solver = om.DirectSolver()
 
     problem.setup(mode=setup_mode)
-    vars = VariableList.from_unconnected_inputs(problem)
-    assert not vars, "These inputs are not provided: %s" % vars.names()
+    variables = VariableList.from_unconnected_inputs(problem)
+    assert not variables, "These inputs are not provided: %s" % variables.names()
 
     problem.run_model()
 
diff --git a/src/fastoad/cmd/tests/test_api.py b/src/fastoad/cmd/tests/test_api.py
index 4510c9257..8b84348c2 100644
--- a/src/fastoad/cmd/tests/test_api.py
+++ b/src/fastoad/cmd/tests/test_api.py
@@ -2,7 +2,7 @@
 Tests for basic API
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -23,11 +23,13 @@
 import pytest
 
 import fastoad.models
+from fastoad.io import DataFile
 from fastoad.openmdao.variables import Variable
 from .. import api
-from ..api import SAMPLE_FILENAME
-from ..exceptions import FastFileExistsError
-from ...io import DataFile
+from ..exceptions import (
+    FastNoAvailableNotebookError,
+    FastPathExistsError,
+)
 
 DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
 RESULTS_FOLDER_PATH = pth.join(pth.dirname(__file__), "results")
@@ -42,16 +44,120 @@ def cleanup():
     Variable.read_variable_descriptions(pth.dirname(fastoad.models.__file__), update_existing=False)
 
 
-def test_generate_configuration_file(cleanup):
-    configuration_file_path = pth.join(RESULTS_FOLDER_PATH, "new_process.yml")
+def test_generate_notebooks_with_no_notebook(cleanup, with_dummy_plugin_2, plugin_root_path):
+    target_path = pth.join(RESULTS_FOLDER_PATH, "notebooks_noplugin")
+
+    with pytest.raises(FastNoAvailableNotebookError):
+        api.generate_notebooks(target_path)
+
+
+def test_generate_notebooks_with_one_plugin(cleanup, with_dummy_plugin_1, plugin_root_path):
+    target_path = pth.join(RESULTS_FOLDER_PATH, "notebooks_1plugin")
+
+    api.generate_notebooks(target_path)
+    assert pth.isfile(pth.join(target_path, "notebook_1.ipynb"))
+
+
+def test_generate_notebooks_with_one_dist_plugin(
+    cleanup, with_dummy_plugin_distribution_1, plugin_root_path
+):
+    target_path = pth.join(RESULTS_FOLDER_PATH, "notebooks_1dist")
+
+    api.generate_notebooks(target_path)
+    assert pth.isfile(pth.join(target_path, "test_plugin_1", "notebook_1.ipynb"))
+    assert pth.isfile(pth.join(target_path, "test_plugin_4", "notebook_1.ipynb"))
+    assert pth.isdir(pth.join(target_path, "test_plugin_4", "data"))
+    assert pth.isfile(pth.join(target_path, "test_plugin_4", "data", "dummy_file.txt"))
+
+
+def test_generate_notebooks(cleanup, with_dummy_plugins, plugin_root_path):
+    target_path = pth.join(RESULTS_FOLDER_PATH, "notebooks")
+
+    # Test distribution specification
+    api.generate_notebooks(target_path, distribution_name="dummy-dist-3")
+    assert pth.isfile(pth.join(target_path, "notebook_2.ipynb"))
+
+    # Test overwrite
+    with pytest.raises(FastPathExistsError):
+        api.generate_notebooks(target_path)
+
+    # ... without specifying distribution
+    api.generate_notebooks(target_path, overwrite=True)
+    assert pth.isfile(pth.join(target_path, "dummy-dist-1", "test_plugin_1", "notebook_1.ipynb"))
+    assert pth.isfile(pth.join(target_path, "dummy-dist-1", "test_plugin_4", "notebook_1.ipynb"))
+    assert pth.isdir(pth.join(target_path, "dummy-dist-1", "test_plugin_4", "data"))
+    assert pth.isfile(
+        pth.join(target_path, "dummy-dist-1", "test_plugin_4", "data", "dummy_file.txt")
+    )
+
+    assert not pth.exists(pth.join(target_path, "dummy-dist-2"))
+
+    assert pth.isfile(pth.join(target_path, "dummy-dist-3", "notebook_2.ipynb"))
+
+
+def test_generate_configuration_file_plugin_1(cleanup, with_dummy_plugins, plugin_root_path):
+    configuration_file_path = pth.join(RESULTS_FOLDER_PATH, "from_plugin_1.yml")
+
+    # No conf file specified because the dist has only one
+    api.generate_configuration_file(
+        configuration_file_path, overwrite=False, distribution_name="dummy-dist-1"
+    )
+    original_file = pth.join(
+        plugin_root_path, "dist_1", "dummy_plugin_1", "configurations", "dummy_conf_1-1.yml"
+    )
+    assert cmp(configuration_file_path, original_file)
 
-    api.generate_configuration_file(configuration_file_path, False)
     # Generating again without forcing overwrite will make it fail
-    with pytest.raises(FastFileExistsError):
-        api.generate_configuration_file(configuration_file_path, False)
-    api.generate_configuration_file(configuration_file_path, True)
+    with pytest.raises(FastPathExistsError):
+        api.generate_configuration_file(
+            configuration_file_path, overwrite=False, distribution_name="dummy-dist-1"
+        )
+
+    # Generating again with overwrite=True should be Ok
+    api.generate_configuration_file(
+        configuration_file_path, overwrite=True, distribution_name="dummy-dist-1"
+    )
+
+
+def test_generate_configuration_file_plugin_1_alone(cleanup, with_dummy_plugin_1):
+    configuration_file_path = pth.join(RESULTS_FOLDER_PATH, "from_plugin_1.yml")
+
+    # No plugin specified only one plugin is available
+    api.generate_configuration_file(configuration_file_path, overwrite=True)
+
+
+def test_generate_configuration_file_plugin_2(cleanup, with_dummy_plugins, plugin_root_path):
+    configuration_file_path = pth.join(RESULTS_FOLDER_PATH, "from_plugin_2.yml")
+
+    api.generate_configuration_file(
+        configuration_file_path,
+        overwrite=True,
+        distribution_name="dummy-dist-2",
+        sample_file_name="dummy_conf_2-1.yml",
+    )
+
+    original_file = pth.join(
+        plugin_root_path, "dist_2", "dummy_plugin_2", "configurations", "dummy_conf_2-1.yml"
+    )
+    assert cmp(configuration_file_path, original_file)
 
-    original_file = pth.join(pth.dirname(api.__file__), "resources", SAMPLE_FILENAME)
+    with pytest.raises(FastPathExistsError):
+        api.generate_configuration_file(
+            configuration_file_path,
+            overwrite=False,
+            distribution_name="dummy-dist-2",
+            sample_file_name="dummy_conf_3-2.yaml",
+        )
+
+    api.generate_configuration_file(
+        configuration_file_path,
+        overwrite=True,
+        distribution_name="dummy-dist-2",
+        sample_file_name="dummy_conf_3-2.yaml",
+    )
+    original_file = pth.join(
+        plugin_root_path, "dist_2", "dummy_plugin_3", "configurations", "dummy_conf_3-2.yaml"
+    )
     assert cmp(configuration_file_path, original_file)
 
 
@@ -68,7 +174,7 @@ def test_generate_inputs(cleanup):
     data.save()
 
     # Generating again without forcing overwrite will make it fail
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.generate_inputs(CONFIGURATION_FILE_PATH, overwrite=False)
 
     input_file_path = api.generate_inputs(
@@ -101,7 +207,7 @@ def test_list_modules(cleanup):
     out_file = pth.join(RESULTS_FOLDER_PATH, "list_modules.txt")
     assert not pth.exists(out_file)
     api.list_modules(CONFIGURATION_FILE_PATH, out_file)
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.list_modules(CONFIGURATION_FILE_PATH, out_file)
     api.list_modules(CONFIGURATION_FILE_PATH, out_file, overwrite=True)
 
@@ -115,7 +221,7 @@ def test_list_modules(cleanup):
     out_file = pth.join(RESULTS_FOLDER_PATH, "list_modules_with_folder.txt")
     assert not pth.exists(out_file)
     api.list_modules(source_folder, out_file)
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.list_modules(source_folder, out_file)
 
     # Testing with single folder
@@ -138,7 +244,7 @@ def test_list_variables(cleanup):
     out_file_path = pth.join(RESULTS_FOLDER_PATH, "list_variables.txt")
     assert not pth.exists(out_file_path)
     api.list_variables(CONFIGURATION_FILE_PATH, out=out_file_path)
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.list_variables(CONFIGURATION_FILE_PATH, out=out_file_path)
     api.list_variables(CONFIGURATION_FILE_PATH, out=out_file_path, overwrite=True)
     assert pth.exists(out_file_path)
@@ -165,7 +271,7 @@ def test_write_n2(cleanup):
     n2_file_path = pth.join(RESULTS_FOLDER_PATH, "other_n2.html")
     api.write_n2(CONFIGURATION_FILE_PATH, n2_file_path)
     # Running again without forcing overwrite of outputs will make it fail
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.write_n2(CONFIGURATION_FILE_PATH, n2_file_path, False)
     api.write_n2(CONFIGURATION_FILE_PATH, n2_file_path, True)
     assert pth.exists(n2_file_path)
@@ -181,7 +287,7 @@ def test_write_xdsm(cleanup):
     xdsm_file_path = pth.join(RESULTS_FOLDER_PATH, "other_xdsm.html")
     api.write_xdsm(CONFIGURATION_FILE_PATH, xdsm_file_path)
     # Running again without forcing overwrite of outputs will make it fail
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.write_xdsm(CONFIGURATION_FILE_PATH, xdsm_file_path, overwrite=False, dry_run=True)
     api.write_xdsm(CONFIGURATION_FILE_PATH, xdsm_file_path, overwrite=True, dry_run=True)
     assert pth.exists(xdsm_file_path)
@@ -193,7 +299,7 @@ def test_evaluate_problem(cleanup):
     )
     api.evaluate_problem(CONFIGURATION_FILE_PATH, False)
     # Running again without forcing overwrite of outputs will make it fail
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.evaluate_problem(CONFIGURATION_FILE_PATH, False)
     problem = api.evaluate_problem(CONFIGURATION_FILE_PATH, True)
     assert problem["f"] == pytest.approx(32.56910089, abs=1e-8)
@@ -211,7 +317,7 @@ def test_optimize_problem(cleanup):
     )
     api.optimize_problem(CONFIGURATION_FILE_PATH, False)
     # Running again without forcing overwrite of outputs will make it fail
-    with pytest.raises(FastFileExistsError):
+    with pytest.raises(FastPathExistsError):
         api.optimize_problem(CONFIGURATION_FILE_PATH, False)
     problem = api.optimize_problem(CONFIGURATION_FILE_PATH, True)
 
diff --git a/src/fastoad/cmd/tests/test_cli.py b/src/fastoad/cmd/tests/test_cli.py
new file mode 100644
index 000000000..e45579ba8
--- /dev/null
+++ b/src/fastoad/cmd/tests/test_cli.py
@@ -0,0 +1,332 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+import os
+import os.path as pth
+from filecmp import cmp
+from shutil import rmtree
+
+import pandas as pd
+import pytest
+from click.testing import CliRunner
+
+from fastoad.cmd.cli import NOTEBOOK_FOLDER_NAME, fast_oad
+
+DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
+RESULTS_FOLDER_PATH = pth.join(pth.dirname(__file__), "results", "cli")
+
+
+@pytest.fixture(scope="module")
+def cleanup():
+    rmtree(RESULTS_FOLDER_PATH, ignore_errors=True)
+    os.makedirs(RESULTS_FOLDER_PATH)  # needed for CliRunner.isolated_filesystem()
+
+
+def test_plugin_info_no_plugin(with_no_plugin):
+    runner = CliRunner()
+    result = runner.invoke(fast_oad, ["plugin_info"])
+
+    assert not result.exception
+    assert result.exit_code == 0
+    assert result.output == "No available FAST-OAD plugin.\n"
+
+
+def test_plugin_info(with_dummy_plugins):
+    runner = CliRunner()
+    result = runner.invoke(fast_oad, ["plugin_info"])
+
+    assert not result.exception
+    assert result.exit_code == 0
+
+    expected_info = pd.DataFrame(
+        dict(
+            installed_package=[f"dummy-dist-{i}" for i in [1, 2, 3]],
+            has_models=[False, True, True],
+            has_notebooks=[True, False, True],
+            configurations=[
+                ["dummy_conf_1-1.yml"],
+                ["dummy_conf_2-1.yml", "dummy_conf_3-1.yml", "dummy_conf_3-2.yaml"],
+                [],
+            ],
+        )
+    )
+    assert result.output == expected_info.to_markdown(index=False) + "\n"
+
+
+def test_gen_conf_no_plugin(with_no_plugin):
+    runner = CliRunner()
+    result = runner.invoke(fast_oad, ["gen_conf", "my_conf.yml"])
+    assert not result.exception
+    assert result.exit_code == 0
+    assert result.output == "This feature needs plugins, but no plugin available.\n"
+
+
+def test_gen_conf_one_plugin(cleanup, with_dummy_plugin_1, plugin_root_path):
+    runner = CliRunner()
+    with runner.isolated_filesystem(temp_dir=RESULTS_FOLDER_PATH) as temp_dir:
+        # Test minimal command =================================================
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml"],
+        )
+        original_file = pth.join(
+            plugin_root_path, "dist_1", "dummy_plugin_1", "configurations", "dummy_conf_1-1.yml"
+        )
+        assert cmp(pth.join(temp_dir, "my_conf.yml"), original_file)
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("has been written.\n")
+
+        # Test overwrite prompt ================================================
+        result = runner.invoke(fast_oad, ["gen_conf", "my_conf.yml"], input="n")
+        assert not result.exception
+        assert result.exit_code == 0
+        assert "Do you want to overwrite it? [y/N]:" in result.output
+        assert result.output.endswith("Operation cancelled.\n")
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(fast_oad, ["gen_conf", "my_conf.yml"], input="y")
+        assert not result.exception
+        assert result.exit_code == 0
+        assert "Do you want to overwrite it? [y/N]:" in result.output
+        assert result.output.endswith("has been written.\n")
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(fast_oad, ["gen_conf", "my_conf.yml", "--force"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert "Do you want to overwrite it? [y/N]:" not in result.output
+        assert result.output.endswith("has been written.\n")
+
+        # Test plugin specification ============================================
+        result = runner.invoke(
+            fast_oad, ["gen_conf", "my_conf.yml", "--from_package", "unknown-dist"], input="n"
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith(
+            'No installed package with FAST-OAD plugin found with name "unknown-dist".\n'
+        )
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(fast_oad, ["gen_conf", "my_conf.yml", "-f", "-p", "dummy-dist-1"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("has been written.\n")
+
+        # Test source file specification =======================================
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml", "-f", "-p", "dummy-dist-1", "-s", "dummy_conf_1-1.yml"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("has been written.\n")
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml", "-f", "--source", "dummy_conf_1-1.yml"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("has been written.\n")
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml", "-f", "--source", "unknown_conf.yml"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith(
+            '"unknown_conf.yml" not provided with installed package "dummy-dist-1".\n'
+        )
+
+
+def test_gen_conf_several_plugin(cleanup, with_dummy_plugins, plugin_root_path):
+    runner = CliRunner()
+    with runner.isolated_filesystem(temp_dir=RESULTS_FOLDER_PATH) as temp_dir:
+        # Test errors ==========================================================
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith(
+            "Several installed packages with FAST-OAD plugins are available. "
+            "One must be specified.\n"
+        )
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml", "-p", "dummy-dist-2"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith(
+            '"dummy-dist-2" provides several configuration files. One must be specified.\n'
+        )
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml", "-p", "dummy-dist-2", "-s", "unknown_conf.yml"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith(
+            '"unknown_conf.yml" not provided with installed package "dummy-dist-2".\n'
+        )
+
+        # Test source file specification =======================================
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml", "-f", "-p", "dummy-dist-1", "-s", "dummy_conf_1-1.yml"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("has been written.\n")
+        original_file = pth.join(
+            plugin_root_path, "dist_1", "dummy_plugin_1", "configurations", "dummy_conf_1-1.yml"
+        )
+        assert cmp(pth.join(temp_dir, "my_conf.yml"), original_file)
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(
+            fast_oad,
+            ["gen_conf", "my_conf.yml", "-f", "-p", "dummy-dist-2", "-s", "dummy_conf_3-2.yaml"],
+        )
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("has been written.\n")
+        original_file = pth.join(
+            plugin_root_path, "dist_2", "dummy_plugin_3", "configurations", "dummy_conf_3-2.yaml"
+        )
+        assert cmp(pth.join(temp_dir, "my_conf.yml"), original_file)
+
+
+def test_create_notebooks_with_no_notebook(cleanup, with_dummy_plugin_2, plugin_root_path):
+    runner = CliRunner()
+    with runner.isolated_filesystem(temp_dir=RESULTS_FOLDER_PATH):
+        result = runner.invoke(fast_oad, ["notebooks"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("No notebook available in FAST-OAD plugins.\n")
+
+
+def test_create_notebooks_with_1_distribution(
+    cleanup, with_dummy_plugin_distribution_1, plugin_root_path
+):
+    runner = CliRunner()
+    with runner.isolated_filesystem(temp_dir=RESULTS_FOLDER_PATH) as temp_dir:
+        notebook_folder = pth.join(temp_dir, NOTEBOOK_FOLDER_NAME)
+
+        # Test basic run =======================================================
+        result = runner.invoke(fast_oad, ["notebooks"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.startswith(f'"{notebook_folder}" has been written')
+        assert pth.isdir(notebook_folder)
+        assert pth.isfile(pth.join(notebook_folder, "test_plugin_1", "notebook_1.ipynb"))
+        assert pth.isfile(pth.join(notebook_folder, "test_plugin_4", "notebook_1.ipynb"))
+        assert pth.isdir(pth.join(temp_dir, NOTEBOOK_FOLDER_NAME, "test_plugin_4", "data"))
+
+    with runner.isolated_filesystem(temp_dir=RESULTS_FOLDER_PATH) as temp_dir:
+        notebook_folder = pth.join(temp_dir, NOTEBOOK_FOLDER_NAME)
+
+        # Test basic run with package specification ============================
+        result = runner.invoke(fast_oad, ["notebooks", "--from_package", "dummy-dist-1"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.startswith(f'"{notebook_folder}" has been written')
+        assert pth.isdir(notebook_folder)
+        assert pth.isfile(pth.join(notebook_folder, "test_plugin_1", "notebook_1.ipynb"))
+        assert pth.isfile(pth.join(notebook_folder, "test_plugin_4", "notebook_1.ipynb"))
+        assert pth.isdir(pth.join(temp_dir, NOTEBOOK_FOLDER_NAME, "test_plugin_4", "data"))
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(fast_oad, ["notebooks", "--from_package", "unknown-dist"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith(
+            'No installed package with FAST-OAD plugin found with name "unknown-dist".\n'
+        )
+
+        # Test overwrite =======================================================
+        result = runner.invoke(fast_oad, ["notebooks"], input="n")
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith("Operation cancelled.\n")
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(fast_oad, ["notebooks"], input="y")
+        assert not result.exception
+        assert result.exit_code == 0
+        assert f'"{notebook_folder}" has been written' in result.output
+
+        # Test path specification ==============================================
+        result = runner.invoke(fast_oad, ["notebooks", "my_path"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert (
+            f'"{pth.join(temp_dir, "my_path", NOTEBOOK_FOLDER_NAME)}" has been written'
+            in result.output
+        )
+
+
+def test_create_notebooks_with_plugins(cleanup, with_dummy_plugins, plugin_root_path):
+    runner = CliRunner()
+    with runner.isolated_filesystem(temp_dir=RESULTS_FOLDER_PATH) as temp_dir:
+        notebook_folder = pth.join(temp_dir, NOTEBOOK_FOLDER_NAME)
+
+        # Test basic run =======================================================
+        result = runner.invoke(fast_oad, ["notebooks"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert pth.isdir(notebook_folder)
+        assert pth.isfile(
+            pth.join(notebook_folder, "dummy-dist-1", "test_plugin_1", "notebook_1.ipynb")
+        )
+        assert pth.isfile(
+            pth.join(notebook_folder, "dummy-dist-1", "test_plugin_4", "notebook_1.ipynb")
+        )
+        assert pth.isdir(
+            pth.join(temp_dir, NOTEBOOK_FOLDER_NAME, "dummy-dist-1", "test_plugin_4", "data")
+        )
+
+        assert pth.isdir(pth.join(notebook_folder, "dummy-dist-3"))
+        assert pth.isfile(pth.join(notebook_folder, "dummy-dist-3", "notebook_2.ipynb"))
+
+    with runner.isolated_filesystem(temp_dir=RESULTS_FOLDER_PATH) as temp_dir:
+        notebook_folder = pth.join(temp_dir, NOTEBOOK_FOLDER_NAME)
+
+        # Test basic run with package specification ============================
+        result = runner.invoke(fast_oad, ["notebooks", "-p", "dummy-dist-1"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.startswith(f'"{notebook_folder}" has been written')
+        assert pth.isdir(notebook_folder)
+        assert pth.isfile(pth.join(notebook_folder, "test_plugin_1", "notebook_1.ipynb"))
+        assert pth.isfile(pth.join(notebook_folder, "test_plugin_4", "notebook_1.ipynb"))
+        assert pth.isdir(pth.join(temp_dir, NOTEBOOK_FOLDER_NAME, "test_plugin_4", "data"))
+
+        # ----------------------------------------------------------------------
+        result = runner.invoke(fast_oad, ["notebooks", "-p", "unknown-dist"])
+        assert not result.exception
+        assert result.exit_code == 0
+        assert result.output.endswith(
+            'No installed package with FAST-OAD plugin found with name "unknown-dist".\n'
+        )
diff --git a/src/fastoad/io/configuration/tests/test_configuration.py b/src/fastoad/io/configuration/tests/test_configuration.py
index 60bc55e62..cfaf66d79 100644
--- a/src/fastoad/io/configuration/tests/test_configuration.py
+++ b/src/fastoad/io/configuration/tests/test_configuration.py
@@ -27,7 +27,7 @@
 from fastoad.io import DataFile
 from fastoad.io.configuration.configuration import FASTOADProblemConfigurator
 from fastoad.module_management._bundle_loader import BundleLoader
-from fastoad.module_management._plugins import load_plugins
+from fastoad.module_management._plugins import FastoadLoader
 from fastoad.module_management.exceptions import FastBundleLoaderUnknownFactoryNameError
 from ..exceptions import (
     FASTConfigurationBadOpenMDAOInstructionError,
@@ -42,7 +42,7 @@
 def cleanup():
     rmtree(RESULTS_FOLDER_PATH, ignore_errors=True)
     yield
-    load_plugins()
+    FastoadLoader.load()
 
 
 def clear_openmdao_registry():
diff --git a/src/fastoad/models/aerodynamics/components/tests/test_components.py b/src/fastoad/models/aerodynamics/components/tests/test_components.py
index 306d0274e..577876798 100644
--- a/src/fastoad/models/aerodynamics/components/tests/test_components.py
+++ b/src/fastoad/models/aerodynamics/components/tests/test_components.py
@@ -23,8 +23,8 @@
 from pytest import approx
 from stdatm import Atmosphere
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
 from ..cd0 import CD0
 from ..cd_compressibility import CdCompressibility
 from ..cd_trim import CdTrim
diff --git a/src/fastoad/models/aerodynamics/external/xfoil/tests/test_xfoil.py b/src/fastoad/models/aerodynamics/external/xfoil/tests/test_xfoil.py
index 885dc6e0a..773ef2c60 100644
--- a/src/fastoad/models/aerodynamics/external/xfoil/tests/test_xfoil.py
+++ b/src/fastoad/models/aerodynamics/external/xfoil/tests/test_xfoil.py
@@ -2,7 +2,7 @@
 Test module for XFOIL component
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -18,24 +18,18 @@
 
 import os.path as pth
 import shutil
-from platform import system
 
 import pytest
 from openmdao.core.indepvarcomp import IndepVarComp
 
-from tests.testing_utilities import run_system
-from tests.xfoil_exe.get_xfoil import get_xfoil_path
-from ..xfoil_polar import XfoilPolar, DEFAULT_2D_CL_MAX
+from fastoad._utils.testing import run_system
+from ..xfoil_polar import DEFAULT_2D_CL_MAX, XfoilPolar
 
 XFOIL_RESULTS = pth.join(pth.dirname(__file__), "results")
 
-xfoil_path = None if system() == "Windows" else get_xfoil_path()
 
-
-@pytest.mark.skipif(
-    system() != "Windows" and xfoil_path is None, reason="No XFOIL executable available"
-)
-def test_compute():
+@pytest.mark.skip_if_no_xfoil()
+def test_compute(xfoil_path):
     """Tests a simple XFOIL run"""
 
     if pth.exists(XFOIL_RESULTS):
@@ -80,10 +74,8 @@ def test_compute():
     assert pth.exists(pth.join(XFOIL_RESULTS, "polar_result.txt"))
 
 
-@pytest.mark.skipif(
-    system() != "Windows" and xfoil_path is None, reason="No XFOIL executable available"
-)
-def test_compute_with_provided_path():
+@pytest.mark.skip_if_no_xfoil()
+def test_compute_with_provided_path(xfoil_path):
     """Test that option "use_exe_path" works"""
     ivc = IndepVarComp()
     ivc.add_output("xfoil:reynolds", 18000000)
diff --git a/src/fastoad/models/aerodynamics/tests/test_aerodynamics_landing.py b/src/fastoad/models/aerodynamics/tests/test_aerodynamics_landing.py
index 2deee8b48..a111c7aa5 100644
--- a/src/fastoad/models/aerodynamics/tests/test_aerodynamics_landing.py
+++ b/src/fastoad/models/aerodynamics/tests/test_aerodynamics_landing.py
@@ -2,7 +2,7 @@
 Test module for aerodynamics groups
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -15,18 +15,14 @@
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 import os.path as pth
-from platform import system
 
 import pytest
 from pytest import approx
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
-from tests.xfoil_exe.get_xfoil import get_xfoil_path
 from ..aerodynamics_landing import AerodynamicsLanding
 
-xfoil_path = None if system() == "Windows" else get_xfoil_path()
-
 
 def get_indep_var_comp(var_names):
     """Reads required input data and returns an IndepVarcomp() instance"""
@@ -36,10 +32,8 @@ def get_indep_var_comp(var_names):
     return ivc
 
 
-@pytest.mark.skipif(
-    system() != "Windows" and xfoil_path is None, reason="No XFOIL executable available"
-)
-def test_aerodynamics_landing_with_xfoil():
+@pytest.mark.skip_if_no_xfoil()
+def test_aerodynamics_landing_with_xfoil(xfoil_path):
     """Tests AerodynamicsHighSpeed"""
     input_list = [
         "data:TLAR:approach_speed",
diff --git a/src/fastoad/models/geometry/tests/test_aero_center.py b/src/fastoad/models/geometry/tests/test_aero_center.py
index 626973ce0..989d4004f 100644
--- a/src/fastoad/models/geometry/tests/test_aero_center.py
+++ b/src/fastoad/models/geometry/tests/test_aero_center.py
@@ -15,8 +15,8 @@
 
 import pytest
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
 from ..compute_aero_center import ComputeAeroCenter
 
 DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
diff --git a/src/fastoad/models/geometry/tests/test_geometry_geom_comps.py b/src/fastoad/models/geometry/tests/test_geometry_geom_comps.py
index 8a93fc6e1..ed1256304 100644
--- a/src/fastoad/models/geometry/tests/test_geometry_geom_comps.py
+++ b/src/fastoad/models/geometry/tests/test_geometry_geom_comps.py
@@ -20,8 +20,8 @@
 
 import pytest
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
 from ..geom_components.compute_wetted_area import ComputeWettedArea
 from ..geom_components.fuselage.compute_cnbeta_fuselage import ComputeCnBetaFuselage
 from ..geom_components.fuselage.compute_fuselage import (
diff --git a/src/fastoad/models/handling_qualities/tests/test_static_margin.py b/src/fastoad/models/handling_qualities/tests/test_static_margin.py
index 5fcd5de69..f9b7092e2 100644
--- a/src/fastoad/models/handling_qualities/tests/test_static_margin.py
+++ b/src/fastoad/models/handling_qualities/tests/test_static_margin.py
@@ -16,8 +16,8 @@
 import openmdao.api as om
 import pytest
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
 from ..compute_static_margin import ComputeStaticMargin
 
 DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
diff --git a/src/fastoad/models/handling_qualities/tests/test_tail_areas.py b/src/fastoad/models/handling_qualities/tests/test_tail_areas.py
index 0625ee494..7a010b8eb 100644
--- a/src/fastoad/models/handling_qualities/tests/test_tail_areas.py
+++ b/src/fastoad/models/handling_qualities/tests/test_tail_areas.py
@@ -15,8 +15,8 @@
 
 import pytest
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
 from ..tail_sizing.compute_ht_area import ComputeHTArea
 from ..tail_sizing.compute_vt_area import ComputeVTArea
 
diff --git a/src/fastoad/models/loops/tests/test_compute_wing_area.py b/src/fastoad/models/loops/tests/test_compute_wing_area.py
index 920c48390..fd9276236 100644
--- a/src/fastoad/models/loops/tests/test_compute_wing_area.py
+++ b/src/fastoad/models/loops/tests/test_compute_wing_area.py
@@ -2,7 +2,7 @@
 test module for wing area computation
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
+#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -19,7 +19,7 @@
 import openmdao.api as om
 from numpy.testing import assert_allclose
 
-from tests.testing_utilities import run_system
+from fastoad._utils.testing import run_system
 from ..compute_wing_area import ComputeWingArea
 
 DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
diff --git a/src/fastoad/models/loops/tests/test_compute_wing_position.py b/src/fastoad/models/loops/tests/test_compute_wing_position.py
index 187d3c29a..28aaf7426 100644
--- a/src/fastoad/models/loops/tests/test_compute_wing_position.py
+++ b/src/fastoad/models/loops/tests/test_compute_wing_position.py
@@ -2,7 +2,7 @@
 test module for wing position computation
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
+#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -19,7 +19,7 @@
 import openmdao.api as om
 from numpy.testing import assert_allclose
 
-from tests.testing_utilities import run_system
+from fastoad._utils.testing import run_system
 from ..compute_wing_position import ComputeWingPosition
 
 DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
diff --git a/src/fastoad/models/performances/mission/openmdao/tests/test_breguet_mission.py b/src/fastoad/models/performances/mission/openmdao/tests/test_breguet_mission.py
index 684919173..ae3729a95 100644
--- a/src/fastoad/models/performances/mission/openmdao/tests/test_breguet_mission.py
+++ b/src/fastoad/models/performances/mission/openmdao/tests/test_breguet_mission.py
@@ -15,7 +15,7 @@
 import openmdao.api as om
 from numpy.testing import assert_allclose
 
-from tests.testing_utilities import run_system
+from fastoad._utils.testing import run_system
 from ..mission import Mission
 
 
diff --git a/src/fastoad/models/performances/mission/openmdao/tests/test_mission.py b/src/fastoad/models/performances/mission/openmdao/tests/test_mission.py
index f44b4a751..343e94e73 100644
--- a/src/fastoad/models/performances/mission/openmdao/tests/test_mission.py
+++ b/src/fastoad/models/performances/mission/openmdao/tests/test_mission.py
@@ -21,11 +21,11 @@
 from openmdao.core.component import Component
 from scipy.constants import foot, knot
 
+from fastoad._utils.testing import run_system
 from fastoad.io import DataFile
 from fastoad.model_base import FlightPoint
 from fastoad.model_base.propulsion import AbstractFuelPropulsion, IOMPropulsionWrapper, IPropulsion
 from fastoad.module_management.service_registry import RegisterPropulsion
-from tests.testing_utilities import run_system
 from ..mission import Mission, MissionComponent
 from ..mission_wrapper import MissionWrapper
 from ...mission_definition.exceptions import FastMissionFileMissingMissionNameError
diff --git a/src/fastoad/models/propulsion/fuel_propulsion/rubber_engine/tests/test_openmdao_engine.py b/src/fastoad/models/propulsion/fuel_propulsion/rubber_engine/tests/test_openmdao_engine.py
index 8582cf2b3..23b452462 100644
--- a/src/fastoad/models/propulsion/fuel_propulsion/rubber_engine/tests/test_openmdao_engine.py
+++ b/src/fastoad/models/propulsion/fuel_propulsion/rubber_engine/tests/test_openmdao_engine.py
@@ -17,8 +17,8 @@
 import numpy as np
 import openmdao.api as om
 
+from fastoad._utils.testing import run_system
 from fastoad.constants import EngineSetting
-from tests.testing_utilities import run_system
 from ..openmdao import OMRubberEngineComponent
 
 
diff --git a/src/fastoad/models/weight/cg/tests/test_cg_components.py b/src/fastoad/models/weight/cg/tests/test_cg_components.py
index 3dda8ab1f..51349409a 100644
--- a/src/fastoad/models/weight/cg/tests/test_cg_components.py
+++ b/src/fastoad/models/weight/cg/tests/test_cg_components.py
@@ -20,8 +20,8 @@
 
 import pytest
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
 from ..cg import ComputeAircraftCG
 from ..cg_components.compute_cg_control_surfaces import ComputeControlSurfacesCG
 from ..cg_components.compute_cg_others import ComputeOthersCG
diff --git a/src/fastoad/models/weight/mass_breakdown/tests/test_mass_breakdown.py b/src/fastoad/models/weight/mass_breakdown/tests/test_mass_breakdown.py
index 82b56a835..80f418a71 100644
--- a/src/fastoad/models/weight/mass_breakdown/tests/test_mass_breakdown.py
+++ b/src/fastoad/models/weight/mass_breakdown/tests/test_mass_breakdown.py
@@ -20,8 +20,8 @@
 import openmdao.api as om
 import pytest
 
+from fastoad._utils.testing import run_system
 from fastoad.io import VariableIO
-from tests.testing_utilities import run_system
 from ..a_airframe import (
     EmpennageWeight,
     FlightControlsWeight,
diff --git a/src/fastoad/module_management/tests/test_plugins.py b/src/fastoad/module_management/tests/test_plugins.py
index 46b4ebefb..4f030cd6c 100644
--- a/src/fastoad/module_management/tests/test_plugins.py
+++ b/src/fastoad/module_management/tests/test_plugins.py
@@ -1,5 +1,5 @@
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -14,55 +14,226 @@
 import os.path as pth
 
 import pytest
-from pkg_resources import EntryPoint, get_distribution
 
-from .._plugins import MODEL_PLUGIN_ID, load_plugins
-from ..exceptions import FastBundleLoaderUnknownFactoryNameError
-from ..service_registry import RegisterSpecializedService
-from ...openmdao.variables import Variable
+from fastoad.openmdao.variables import Variable
+from .._plugins import FastoadLoader, SubPackageNames
+from ..exceptions import (
+    FastBundleLoaderUnknownFactoryNameError,
+    FastNoDistPluginError,
+    FastSeveralConfigurationFilesError,
+    FastSeveralDistPluginsError,
+    FastUnknownConfigurationFileError,
+    FastUnknownDistPluginError,
+)
+from ..service_registry import RegisterService
 
 DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
 
 
-# Initialization of services ###############
-class DummyBase:
-    def my_class(self):
-        return self.__class__.__name__
+# Tests ####################################
+def test_plugins(with_dummy_plugins):
 
+    FastoadLoader._loaded = True  # Ensures next instantiation will NOT trigger reloading
 
-class RegisterDummyService(RegisterSpecializedService, base_class=DummyBase):
-    pass
+    # Before FastoadLoader.load(), services are not registered
+    with pytest.raises(FastBundleLoaderUnknownFactoryNameError):
+        declared_dummy_1 = RegisterService.get_provider("test.plugin.declared.1")
+    with pytest.raises(FastBundleLoaderUnknownFactoryNameError):
+        decorated_dummy_1 = RegisterService.get_provider("test.plugin.decorated.1")
 
+    FastoadLoader._loaded = False  # Ensures next instantiation will trigger reloading
 
-# Tests ####################################
-def test_plugins():
-    # Declaring the plugin
-    dist = get_distribution("FAST-OAD")
-    dummy_plugin = EntryPoint(
-        "test_plugin", "fastoad.module_management.tests.data.dummy_plugin", dist=dist
-    )
-    dist.get_entry_map(MODEL_PLUGIN_ID)["test_plugin"] = dummy_plugin
+    dist1_definition = FastoadLoader().distribution_plugin_definitions["dummy-dist-1"]
+    assert "models" not in dist1_definition["test_plugin_1"].subpackages
+    assert "notebooks" not in dist1_definition["test_plugin_1"].subpackages
 
-    # Before load_plugins(), services are not registered
-    with pytest.raises(FastBundleLoaderUnknownFactoryNameError):
-        declared_dummy_1 = RegisterDummyService.get_provider("test.plugin.declared.1")
-    with pytest.raises(FastBundleLoaderUnknownFactoryNameError):
-        decorated_dummy_1 = RegisterDummyService.get_provider("test.plugin.decorated.1")
+    assert "models" not in dist1_definition["test_plugin_4"].subpackages
+    assert "configurations" not in dist1_definition["test_plugin_4"].subpackages
 
-    load_plugins()
+    dist2_definition = FastoadLoader().distribution_plugin_definitions["dummy-dist-2"]
+    assert (
+        dist2_definition["test_plugin_3"].subpackages[SubPackageNames.MODELS]
+        == "tests.dummy_plugins.dist_2.dummy_plugin_3.models"
+    )
+    assert "notebooks" not in dist2_definition["test_plugin_3"].subpackages
+    assert (
+        dist2_definition["test_plugin_3"].subpackages[SubPackageNames.CONFIGURATIONS]
+        == "tests.dummy_plugins.dist_2.dummy_plugin_3.configurations"
+    )
 
-    declared_dummy_1 = RegisterDummyService.get_provider("test.plugin.declared.1")
+    declared_dummy_1 = RegisterService.get_provider("test.plugin.declared.1")
     assert declared_dummy_1.my_class() == "DeclaredDummy1"
-    declared_dummy_2 = RegisterDummyService.get_provider("test.plugin.declared.2")
+    declared_dummy_2 = RegisterService.get_provider("test.plugin.declared.2")
     assert declared_dummy_2.my_class() == "DeclaredDummy2"
 
-    decorated_dummy_1 = RegisterDummyService.get_provider("test.plugin.decorated.1")
+    decorated_dummy_1 = RegisterService.get_provider("test.plugin.decorated.1")
     assert decorated_dummy_1.my_class() == "DecoratedDummy1"
-    decorated_dummy_2 = RegisterDummyService.get_provider("test.plugin.decorated.2")
+    decorated_dummy_2 = RegisterService.get_provider("test.plugin.decorated.2")
     assert decorated_dummy_2.my_class() == "DecoratedDummy2"
     # This one is in a subpackage
-    decorated_dummy_3 = RegisterDummyService.get_provider("test.plugin.decorated.3")
+    decorated_dummy_3 = RegisterService.get_provider("test.plugin.decorated.3")
     assert decorated_dummy_3.my_class() == "DecoratedDummy3"
 
     # Checking variable description.
     assert Variable("dummy:variable").description == "Some dummy variable."
+
+
+def test_get_distribution_plugin_definition_with_0_plugin(with_no_plugin):
+    with pytest.raises(FastNoDistPluginError):
+        FastoadLoader().get_distribution_plugin_definition()
+    with pytest.raises(FastNoDistPluginError):
+        FastoadLoader().get_distribution_plugin_definition("anything")
+
+
+def test_get_distribution_plugin_definition_with_1_plugin(with_dummy_plugin_1):
+    with pytest.raises(FastUnknownDistPluginError):
+        FastoadLoader().get_distribution_plugin_definition("unknown")
+
+    dist_def = FastoadLoader().get_distribution_plugin_definition("dummy-dist-1")
+    assert dist_def.dist_name == "dummy-dist-1"
+    assert set(dist_def.keys()) == {"test_plugin_1"}
+    assert dist_def["test_plugin_1"].package_name == "tests.dummy_plugins.dist_1.dummy_plugin_1"
+
+    dist_def_bis = FastoadLoader().get_distribution_plugin_definition()
+    assert dist_def == dist_def_bis
+
+
+def test_get_distribution_plugin_definition_with_plugins(with_dummy_plugins):
+    with pytest.raises(FastSeveralDistPluginsError):
+        FastoadLoader().get_distribution_plugin_definition()
+    with pytest.raises(FastUnknownDistPluginError):
+        FastoadLoader().get_distribution_plugin_definition("unknown")
+
+    dist_def = FastoadLoader().get_distribution_plugin_definition("dummy-dist-1")
+    assert dist_def.dist_name == "dummy-dist-1"
+    assert set(dist_def.keys()) == {"test_plugin_1", "test_plugin_4"}
+    assert dist_def["test_plugin_1"].package_name == "tests.dummy_plugins.dist_1.dummy_plugin_1"
+    assert dist_def["test_plugin_4"].package_name == "tests.dummy_plugins.dist_1.dummy_plugin_4"
+
+    dist_def = FastoadLoader().get_distribution_plugin_definition("dummy-dist-2")
+    assert dist_def.dist_name == "dummy-dist-2"
+    assert set(dist_def.keys()) == {"test_plugin_2", "test_plugin_3"}
+    assert dist_def["test_plugin_2"].package_name == "tests.dummy_plugins.dist_2.dummy_plugin_2"
+    assert dist_def["test_plugin_3"].package_name == "tests.dummy_plugins.dist_2.dummy_plugin_3"
+
+
+def test_get_plugin_configuration_file_list(with_dummy_plugins):
+    def extract_info(file_list):
+        return {(item.name, item.plugin_name) for item in file_list}
+
+    file_list = FastoadLoader().get_configuration_file_list("dummy-dist-1")
+    assert extract_info(file_list) == {("dummy_conf_1-1.yml", "test_plugin_1")}
+
+    file_list = FastoadLoader().get_configuration_file_list("dummy-dist-2")
+    assert extract_info(file_list) == {
+        ("dummy_conf_2-1.yml", "test_plugin_2"),
+        ("dummy_conf_3-1.yml", "test_plugin_3"),
+        ("dummy_conf_3-2.yaml", "test_plugin_3"),
+    }
+
+    file_list = FastoadLoader().get_configuration_file_list("dummy-dist-3")
+    assert extract_info(file_list) == set()
+
+    # improper name
+    with pytest.raises(FastUnknownDistPluginError):
+        file_list = FastoadLoader().get_configuration_file_list("unknown-dist")
+
+
+def test_get_plugin_notebook_folder_list_with_one_plugin(with_dummy_plugin_distribution_1):
+    def extract_info(folder_list):
+        return {(item.dist_name, item.package_name) for item in folder_list}
+
+    folder_list = FastoadLoader().get_notebook_folder_list()
+    assert extract_info(folder_list) == {
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_1.notebooks"),
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_4.notebooks"),
+    }
+    folder_list = FastoadLoader().get_notebook_folder_list("dummy-dist-1")
+    assert extract_info(folder_list) == {
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_1.notebooks"),
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_4.notebooks"),
+    }
+    # improper name
+    with pytest.raises(FastUnknownDistPluginError):
+        file_list = FastoadLoader().get_notebook_folder_list("unknown-dist")
+
+
+def test_get_plugin_notebook_folder_list_with_plugins(with_dummy_plugins):
+    def extract_info(folder_list):
+        return {(item.dist_name, item.package_name) for item in folder_list}
+
+    folder_list = FastoadLoader().get_notebook_folder_list()
+    assert extract_info(folder_list) == {
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_1.notebooks"),
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_4.notebooks"),
+        ("dummy-dist-3", "tests.dummy_plugins.dist_3.dummy_plugin_5.notebooks"),
+    }
+    folder_list = FastoadLoader().get_notebook_folder_list("dummy-dist-1")
+    assert extract_info(folder_list) == {
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_1.notebooks"),
+        ("dummy-dist-1", "tests.dummy_plugins.dist_1.dummy_plugin_4.notebooks"),
+    }
+    folder_list = FastoadLoader().get_notebook_folder_list("dummy-dist-2")
+    assert extract_info(folder_list) == set()
+    folder_list = FastoadLoader().get_notebook_folder_list("dummy-dist-3")
+    assert extract_info(folder_list) == {
+        ("dummy-dist-3", "tests.dummy_plugins.dist_3.dummy_plugin_5.notebooks")
+    }
+
+    # improper name
+    with pytest.raises(FastUnknownDistPluginError):
+        file_list = FastoadLoader().get_notebook_folder_list("unknown-dist")
+
+
+def test_get_configuration_file_info_with_1_plugin(with_dummy_plugin_1):
+    dist_def = FastoadLoader().get_distribution_plugin_definition("dummy-dist-1")
+
+    with pytest.raises(FastUnknownConfigurationFileError):
+        dist_def.get_configuration_file_info("unknown.yml")
+    file_info = dist_def.get_configuration_file_info("dummy_conf_1-1.yml")
+    assert file_info.name == "dummy_conf_1-1.yml"
+    assert file_info.dist_name == "dummy-dist-1"
+    assert file_info.plugin_name == "test_plugin_1"
+    assert file_info.package_name == "tests.dummy_plugins.dist_1.dummy_plugin_1.configurations"
+
+    file_info_bis = dist_def.get_configuration_file_info()
+    assert file_info_bis == file_info
+
+
+def test_get_configuration_file_info_with_plugins(with_dummy_plugins):
+    dist_def = FastoadLoader().get_distribution_plugin_definition("dummy-dist-1")
+
+    with pytest.raises(FastUnknownConfigurationFileError):
+        dist_def.get_configuration_file_info("unknown.yml")
+    file_info = dist_def.get_configuration_file_info("dummy_conf_1-1.yml")
+    assert file_info.name == "dummy_conf_1-1.yml"
+    assert file_info.dist_name == "dummy-dist-1"
+    assert file_info.plugin_name == "test_plugin_1"
+    assert file_info.package_name == "tests.dummy_plugins.dist_1.dummy_plugin_1.configurations"
+
+    file_info_bis = dist_def.get_configuration_file_info()
+    assert file_info_bis == file_info
+
+    dist_def = FastoadLoader().get_distribution_plugin_definition("dummy-dist-2")
+    with pytest.raises(FastUnknownConfigurationFileError):
+        dist_def.get_configuration_file_info("unknown.yml")
+    with pytest.raises(FastSeveralConfigurationFilesError):
+        dist_def.get_configuration_file_info()
+
+    file_info = dist_def.get_configuration_file_info("dummy_conf_2-1.yml")
+    assert file_info.name == "dummy_conf_2-1.yml"
+    assert file_info.dist_name == "dummy-dist-2"
+    assert file_info.plugin_name == "test_plugin_2"
+    assert file_info.package_name == "tests.dummy_plugins.dist_2.dummy_plugin_2.configurations"
+
+    file_info = dist_def.get_configuration_file_info("dummy_conf_3-1.yml")
+    assert file_info.name == "dummy_conf_3-1.yml"
+    assert file_info.dist_name == "dummy-dist-2"
+    assert file_info.plugin_name == "test_plugin_3"
+    assert file_info.package_name == "tests.dummy_plugins.dist_2.dummy_plugin_3.configurations"
+
+    file_info = dist_def.get_configuration_file_info("dummy_conf_3-2.yaml")
+    assert file_info.name == "dummy_conf_3-2.yaml"
+    assert file_info.dist_name == "dummy-dist-2"
+    assert file_info.plugin_name == "test_plugin_3"
+    assert file_info.package_name == "tests.dummy_plugins.dist_2.dummy_plugin_3.configurations"
diff --git a/tests/conftest.py b/tests/conftest.py
index 760e624b1..a6cc6cf86 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,8 +1,5 @@
-"""
-    Basic settings for tests
-"""
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -13,3 +10,6 @@
 #  GNU General Public License for more details.
 #  You should have received a copy of the GNU General Public License
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+# pylint: disable=unused-import
+from src.conftest import no_xfoil_skip, xfoil_path  # noqa: F401
diff --git a/tests/dummy_plugins/README.md b/tests/dummy_plugins/README.md
new file mode 100644
index 000000000..c99e75bab
--- /dev/null
+++ b/tests/dummy_plugins/README.md
@@ -0,0 +1,3 @@
+This folder defines dummy plugins for test purposes.
+See `/conftest.py` for fixtures that declare these plugins for the duration
+of a test function.
diff --git a/src/fastoad/module_management/tests/data/dummy_plugin/subpackage/__init__.py b/tests/dummy_plugins/__init__.py
similarity index 94%
rename from src/fastoad/module_management/tests/data/dummy_plugin/subpackage/__init__.py
rename to tests/dummy_plugins/__init__.py
index 385ae0611..f519d3755 100644
--- a/src/fastoad/module_management/tests/data/dummy_plugin/subpackage/__init__.py
+++ b/tests/dummy_plugins/__init__.py
@@ -1,5 +1,5 @@
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
diff --git a/tests/dummy_plugins/base.py b/tests/dummy_plugins/base.py
new file mode 100644
index 000000000..f662d52bb
--- /dev/null
+++ b/tests/dummy_plugins/base.py
@@ -0,0 +1,23 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+from fastoad.module_management.service_registry import RegisterSpecializedService
+
+
+class DummyBase:
+    def my_class(self):
+        return self.__class__.__name__
+
+
+class RegisterDummyService(RegisterSpecializedService, base_class=DummyBase):
+    pass
diff --git a/tests/xfoil_exe/__init__.py b/tests/dummy_plugins/dist_1/__init__.py
similarity index 94%
rename from tests/xfoil_exe/__init__.py
rename to tests/dummy_plugins/dist_1/__init__.py
index 94845a3e6..f519d3755 100644
--- a/tests/xfoil_exe/__init__.py
+++ b/tests/dummy_plugins/dist_1/__init__.py
@@ -1,5 +1,5 @@
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_1/__init__.py b/tests/dummy_plugins/dist_1/dummy_plugin_1/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_1/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_1/configurations/__init__.py b/tests/dummy_plugins/dist_1/dummy_plugin_1/configurations/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_1/configurations/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_1/configurations/dummy_conf_1-1.yml b/tests/dummy_plugins/dist_1/dummy_plugin_1/configurations/dummy_conf_1-1.yml
new file mode 100644
index 000000000..05817cffc
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_1/configurations/dummy_conf_1-1.yml
@@ -0,0 +1,2 @@
+plugin: 1
+number: 1
\ No newline at end of file
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_1/notebooks/__init__.py b/tests/dummy_plugins/dist_1/dummy_plugin_1/notebooks/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_1/notebooks/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_1/notebooks/notebook_1.ipynb b/tests/dummy_plugins/dist_1/dummy_plugin_1/notebooks/notebook_1.ipynb
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_4/__init__.py b/tests/dummy_plugins/dist_1/dummy_plugin_4/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_4/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/__init__.py b/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/data/__init__.py b/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/data/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/data/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/data/dummy_file.txt b/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/data/dummy_file.txt
new file mode 100644
index 000000000..40df018a4
--- /dev/null
+++ b/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/data/dummy_file.txt
@@ -0,0 +1,1 @@
+dummy data = 42
\ No newline at end of file
diff --git a/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/notebook_1.ipynb b/tests/dummy_plugins/dist_1/dummy_plugin_4/notebooks/notebook_1.ipynb
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/dummy_plugins/dist_2/__init__.py b/tests/dummy_plugins/dist_2/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_2/__init__.py b/tests/dummy_plugins/dist_2/dummy_plugin_2/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_2/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_2/configurations/__init__.py b/tests/dummy_plugins/dist_2/dummy_plugin_2/configurations/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_2/configurations/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_2/configurations/dummy_conf_2-1.yml b/tests/dummy_plugins/dist_2/dummy_plugin_2/configurations/dummy_conf_2-1.yml
new file mode 100644
index 000000000..22ca20827
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_2/configurations/dummy_conf_2-1.yml
@@ -0,0 +1,2 @@
+plugin: 2
+number: 1
\ No newline at end of file
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_3/__init__.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/__init__.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/dummy_conf_3-1.yml b/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/dummy_conf_3-1.yml
new file mode 100644
index 000000000..b36e95f81
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/dummy_conf_3-1.yml
@@ -0,0 +1,2 @@
+plugin: 3
+number: 1
\ No newline at end of file
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/dummy_conf_3-2.yaml b/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/dummy_conf_3-2.yaml
new file mode 100644
index 000000000..11f30cb40
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/configurations/dummy_conf_3-2.yaml
@@ -0,0 +1,2 @@
+plugin: 3
+number: 2
\ No newline at end of file
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_3/models/__init__.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/src/fastoad/module_management/tests/data/dummy_plugin/declared_dummy_components.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/declared_dummy_components.py
similarity index 91%
rename from src/fastoad/module_management/tests/data/dummy_plugin/declared_dummy_components.py
rename to tests/dummy_plugins/dist_2/dummy_plugin_3/models/declared_dummy_components.py
index 1aa4e8984..fce1ba3de 100644
--- a/src/fastoad/module_management/tests/data/dummy_plugin/declared_dummy_components.py
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/declared_dummy_components.py
@@ -2,7 +2,7 @@
 Module where services are declared externally (in the __init__.py)
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -14,8 +14,7 @@
 #  You should have received a copy of the GNU General Public License
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-
-from ...test_plugins import DummyBase
+from ....base import DummyBase
 
 
 class DeclaredDummy1(DummyBase):
diff --git a/src/fastoad/module_management/tests/data/dummy_plugin/decorated_dummy_components.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/decorated_dummy_components.py
similarity index 89%
rename from src/fastoad/module_management/tests/data/dummy_plugin/decorated_dummy_components.py
rename to tests/dummy_plugins/dist_2/dummy_plugin_3/models/decorated_dummy_components.py
index 6a8e577e2..694832b8b 100644
--- a/src/fastoad/module_management/tests/data/dummy_plugin/decorated_dummy_components.py
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/decorated_dummy_components.py
@@ -2,7 +2,7 @@
 Module in a subpackage where services are declared with decorators
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -14,8 +14,7 @@
 #  You should have received a copy of the GNU General Public License
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
-
-from ...test_plugins import RegisterDummyService, DummyBase
+from ....base import DummyBase, RegisterDummyService
 
 
 @RegisterDummyService("test.plugin.decorated.3")
diff --git a/src/fastoad/module_management/tests/data/dummy_plugin/register.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/register.py
similarity index 90%
rename from src/fastoad/module_management/tests/data/dummy_plugin/register.py
rename to tests/dummy_plugins/dist_2/dummy_plugin_3/models/register.py
index 2496f09d4..a455c0d35 100644
--- a/src/fastoad/module_management/tests/data/dummy_plugin/register.py
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/register.py
@@ -1,5 +1,5 @@
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -12,7 +12,7 @@
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 from .declared_dummy_components import DeclaredDummy1, DeclaredDummy2
-from ...test_plugins import RegisterDummyService
+from ....base import RegisterDummyService
 
 RegisterDummyService("test.plugin.declared.1")(DeclaredDummy1)
 RegisterDummyService("test.plugin.declared.2")(DeclaredDummy2)
diff --git a/tests/dummy_plugins/dist_2/dummy_plugin_3/models/subpackage/__init__.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/subpackage/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/subpackage/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/src/fastoad/module_management/tests/data/dummy_plugin/subpackage/decorated_dummy_components.py b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/subpackage/decorated_dummy_components.py
similarity index 90%
rename from src/fastoad/module_management/tests/data/dummy_plugin/subpackage/decorated_dummy_components.py
rename to tests/dummy_plugins/dist_2/dummy_plugin_3/models/subpackage/decorated_dummy_components.py
index 25ac4ce27..3c8978d54 100644
--- a/src/fastoad/module_management/tests/data/dummy_plugin/subpackage/decorated_dummy_components.py
+++ b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/subpackage/decorated_dummy_components.py
@@ -2,7 +2,7 @@
 Module where services are declared with decorators
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -15,7 +15,7 @@
 #  along with this program.  If not, see <https://www.gnu.org/licenses/>.
 
 
-from ....test_plugins import RegisterDummyService, DummyBase
+from .....base import DummyBase, RegisterDummyService
 
 
 @RegisterDummyService("test.plugin.decorated.1")
diff --git a/src/fastoad/module_management/tests/data/dummy_plugin/variable_descriptions.txt b/tests/dummy_plugins/dist_2/dummy_plugin_3/models/variable_descriptions.txt
similarity index 100%
rename from src/fastoad/module_management/tests/data/dummy_plugin/variable_descriptions.txt
rename to tests/dummy_plugins/dist_2/dummy_plugin_3/models/variable_descriptions.txt
diff --git a/tests/dummy_plugins/dist_3/__init__.py b/tests/dummy_plugins/dist_3/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_3/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_3/dummy_plugin_5/__init__.py b/tests/dummy_plugins/dist_3/dummy_plugin_5/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_3/dummy_plugin_5/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_3/dummy_plugin_5/models/__init__.py b/tests/dummy_plugins/dist_3/dummy_plugin_5/models/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_3/dummy_plugin_5/models/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_3/dummy_plugin_5/notebooks/__init__.py b/tests/dummy_plugins/dist_3/dummy_plugin_5/notebooks/__init__.py
new file mode 100644
index 000000000..f519d3755
--- /dev/null
+++ b/tests/dummy_plugins/dist_3/dummy_plugin_5/notebooks/__init__.py
@@ -0,0 +1,12 @@
+#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
+#  FAST is free software: you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation, either version 3 of the License, or
+#  (at your option) any later version.
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#  You should have received a copy of the GNU General Public License
+#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
diff --git a/tests/dummy_plugins/dist_3/dummy_plugin_5/notebooks/notebook_2.ipynb b/tests/dummy_plugins/dist_3/dummy_plugin_5/notebooks/notebook_2.ipynb
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/integration_tests/oad_process/test_oad_process.py b/tests/integration_tests/oad_process/test_oad_process.py
index 0a6d18088..66fbf0b97 100644
--- a/tests/integration_tests/oad_process/test_oad_process.py
+++ b/tests/integration_tests/oad_process/test_oad_process.py
@@ -2,7 +2,7 @@
 Test module for Overall Aircraft Design process
 """
 #  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2021 ONERA & ISAE-SUPAERO
+#  Copyright (C) 2022 ONERA & ISAE-SUPAERO
 #  FAST is free software: you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
 #  the Free Software Foundation, either version 3 of the License, or
@@ -34,13 +34,10 @@
     _IConfigurationModifier,
 )
 from tests import root_folder_path
-from tests.xfoil_exe.get_xfoil import get_xfoil_path
 
 DATA_FOLDER_PATH = pth.join(pth.dirname(__file__), "data")
 RESULTS_FOLDER_PATH = pth.join(pth.dirname(__file__), "results")
 
-xfoil_path = None if system() == "Windows" else get_xfoil_path()
-
 
 @pytest.fixture(scope="module")
 def cleanup():
@@ -75,12 +72,13 @@ def test_oad_process(cleanup):
     _check_weight_performance_loop(problem)
 
 
-def test_non_regression_breguet(cleanup):
+def test_non_regression_breguet(cleanup, xfoil_path):
     run_non_regression_test(
         "oad_process_breguet.yml",
         "CeRAS01_legacy_breguet_result.xml",
         "non_regression_breguet",
         use_xfoil=True,
+        xfoil_path=xfoil_path,
     )
 
 
@@ -114,12 +112,13 @@ class XFOILConfigurator(_IConfigurationModifier):
     """Overwrite XFOIL usage setting of configuration file"""
 
     use_xfoil: bool
+    xfoil_path: str = None
 
     def modify(self, problem: om.Problem):
-        if self.use_xfoil and (system() == "Windows" or xfoil_path):
+        if self.use_xfoil and (system() == "Windows" or self.xfoil_path):
             problem.model.aerodynamics_landing._OPTIONS["use_xfoil"] = True
             if system() != "Windows":
-                problem.model.aerodynamics_landing._OPTIONS["xfoil_exe_path"] = xfoil_path
+                problem.model.aerodynamics_landing._OPTIONS["xfoil_exe_path"] = self.xfoil_path
             # BTW we narrow computed alpha range for sake of CPU time
             problem.model.aerodynamics_landing._OPTIONS["xfoil_alpha_min"] = 16.0
             problem.model.aerodynamics_landing._OPTIONS["xfoil_alpha_max"] = 22.0
@@ -130,6 +129,7 @@ def run_non_regression_test(
     legacy_result_file,
     result_dir,
     use_xfoil=False,
+    xfoil_path=None,
     global_tolerance=1e-2,
     vars_to_check=None,
     specific_tolerance=5.0e-3,
@@ -141,6 +141,7 @@ def run_non_regression_test(
     :param legacy_result_file: reference data for inputs and outputs
     :param result_dir: relative name, folder will be in RESULTS_FOLDER_PATH
     :param use_xfoil: if True, XFOIL computation will be activated
+    :param xfoil_path: used if use_xfoil==True
     :param vars_to_check: variables that will be concerned by specific_tolerance
     :param specific_tolerance: test will fail if absolute relative error between computed and
                                reference values is beyond this value for variables in vars_to_check
@@ -155,7 +156,7 @@ def run_non_regression_test(
     api.generate_configuration_file(configuration_file_path)  # just ensure folders are created...
     shutil.copy(pth.join(DATA_FOLDER_PATH, conf_file), configuration_file_path)
     configurator = FASTOADProblemConfigurator(configuration_file_path)
-    configurator._set_configuration_modifier(XFOILConfigurator(use_xfoil))
+    configurator._set_configuration_modifier(XFOILConfigurator(use_xfoil, xfoil_path))
 
     # Generation of inputs ----------------------------------------
     ref_inputs = pth.join(DATA_FOLDER_PATH, legacy_result_file)
diff --git a/tests/xfoil_exe/get_xfoil.py b/tests/xfoil_exe/get_xfoil.py
deleted file mode 100644
index c32bf8788..000000000
--- a/tests/xfoil_exe/get_xfoil.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#  This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design
-#  Copyright (C) 2020  ONERA & ISAE-SUPAERO
-#  FAST is free software: you can redistribute it and/or modify
-#  it under the terms of the GNU General Public License as published by
-#  the Free Software Foundation, either version 3 of the License, or
-#  (at your option) any later version.
-#  This program is distributed in the hope that it will be useful,
-#  but WITHOUT ANY WARRANTY; without even the implied warranty of
-#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-#  GNU General Public License for more details.
-#  You should have received a copy of the GNU General Public License
-#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
-
-import os.path as pth
-from shutil import which
-from typing import Optional
-
-
-def get_xfoil_path() -> Optional[str]:
-    path = pth.join(pth.dirname(__file__), "xfoil")
-    if pth.exists(path):
-        # If there is a local xfoil, use it
-        return path
-    else:
-        # Otherwise, use one that is in PATH, if it exists
-        return which("xfoil")
diff --git a/tests/xfoil_exe/readme.md b/tests/xfoil_exe/readme.md
index 722a87432..96c8886fa 100644
--- a/tests/xfoil_exe/readme.md
+++ b/tests/xfoil_exe/readme.md
@@ -1,4 +1,4 @@
-This folder is here for putting an alternate xfoil executable that can be used during tests.
+This folder is here for putting an alternate xfoil executable that can be used during tests (see pytest fixtures in conftest.py).
 
 As FAST-OAD will embed only the official Windows build of XFOIL, it allows to run the full test suite with XFOIL built 
 for another system.
\ No newline at end of file