Writing your first script ========================= This tutorial will guide you through discovering Quixote and its concepts, in order to allow you to write your own moulinettes easily. Its only pre-requisites are a working installation of Quixote (see `Installing `__), and basic knowledge of programming. .. _what-is-a-moulinette-: What is a moulinette ? ---------------------- A moulinette describes operations that should be applied to a particular piece of data, in order to produce a result. A typical example would be in the context of the validation of an exercise. With **Quixote**, a moulinette is a set of scripts and resources specifying the job's execution environment and behavior. All of these are described in a ``blueprint.py`` file which is called the moulinette's **blueprint**. The steps to be taken during a moulinette's execution are grouped into three phases: - The **build** phase, which specifies how the job's environment should be configured (installed applications, dependencies, ...) - The **fetch** phase, which takes care of fetching the data to be processed by the moulinette - The **inspection** phase, which analyzes the data and produces a result Each phase can of course include multiple steps. .. _how-does-it-look-like-on-my-computer-: How does it look like on my computer ? -------------------------------------- From the Quixote user's point-of-view, the moulinette is simply a directory containing the ``blueprint.py`` script. Optionally, the moulinette can also come with resources, which are to be put into a ``resources`` subdirectory. The following output of the ``tree`` command illustrates an example of the directory structure of a moulinette: :: $> tree my_moulinette my_moulinette ├── blueprint.py └── resources ├── a_cool_resource.txt └── a_subdirectory └── another_resource.jpg 2 directories, 3 files Writing a blueprint file ------------------------ The metadata ~~~~~~~~~~~~ The first element of a blueprint is its metadata, which includes its name, its author, etc. Within the ``blueprint.py`` file, this is represented by an instance of the :class:`quixote.Blueprint` class. It can be created like shown below: .. code-block:: python import quixote blueprint = quixote.Blueprint( name="my_is_even_demo_moulinette", author="Clément 'Doom' Doumergue", ) The phases and the steps ~~~~~~~~~~~~~~~~~~~~~~~~ The second part of the blueprint is about describing the different steps of the moulinette. Since a step is basically a sequence of instructions to perform, it is implemented by defining a Python function inside the blueprint script. .. code-block:: python def almost_a_cool_step(): pass # This step doesn't do anything :/ As we said before, each step must be part of one of the three phases of moulinette execution. Thus, we also need to mark our step functions appropriately, in order to point out the phase they belong to. In the script file, we mark functions as part of a phase like so (using one of the three Python decorators provided by Quixote): .. code-block:: python :emphasize-lines: 1,3,7,11 import quixote @quixote.builder def bonkers_builder(): pass # Nothing to configure @quixote.fetcher def funky_fetcher(): pass # No data to fetch @quixote.inspector def incredible_inspector(): pass # Nothing to analyze def not_a_step(): pass # Not marked, so not a step .. note:: Functions that are not marked are not executed by Quixote, but they still can be called by other functions. When the moulinette is run, phases are executed in order: first, the build phase, then the fetch phase, and finally the inspection phase. .. note:: It is possible to mark multiple functions as steps for one phase. For a given phase, the functions are executed in the order in which they are defined (i.e. from top to bottom). Getting information from the context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to reach resource files or other information about the current moulinette, Quixote provides a context as a dictionary, and allows the steps to retrieve data from it. For example, we can use the following snippet to retrieve the path to a particular resource: .. code-block:: python resources_path = get_context()["resources_path"] file_path = f"{resources_path}/a_cool_resource.txt" Some values are always guaranteed to be found in the context, such as the ``activity_id``, the ``module_id``, and the targeted ``group_id``, as integers. Some other values can only be found during particular phases, like the ``delivery_path``, at which the delivery files are stored (available during the fetch and inspection phases). .. note:: A cheat sheet of the available values for each phase is available :doc:`here `. Implementing a step in the build phase ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Steps belonging to the build phase shall **describe the environment and configuration** required in order to run the moulinette. When the moulinette is prepared, instructions in these steps are translated into concrete instructions (suitable for a Dockerfile, or a shell script for instance), in order to build the moulinette's environment. For example, one can use the :mod:`~quixote.build.apt` built-in to download and install software into the moulinette's environment. Here is an example of a builder step that installs GCC: .. code-block:: python import quixote.build.apt as apt @quixote.builder def install_gcc(): """ Install the GCC compiler using the APT package manager. """ apt.update() apt.install("gcc") .. note:: The :mod:`quixote` package offers three sub-packages named respectively :mod:`~quixote.build`, :mod:`~quixote.fetch` and :mod:`~quixote.inspection`, each providing built-ins for the corresponding phase. The :mod:`quixote.build` module offers a wide range of built-ins for this phase, but it can also be easily extended (see :doc:`Writing your own "build" built-in `). Implementing a step in the fetch phase ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Steps marked as part of the fetch phase are in charge of **fetching all the required data** that must be analyzed by the moulinette. (These are run outside of the moulinette's environment). In most cases, the data is a student delivery that must be fetched from a GitLab repository. This simple task can be achieved using the GitLab built-in. .. code-block:: python import quixote.fetch.gitlab as fetch @quixote.fetcher def fetch_delivery(): fetch.gitlab() .. note:: That built-in uses the moulinette's context to figure out which delivery should be fetched in which module. Implementing a step in the inspection phase ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The inspection steps are the most feature-rich in Quixote. They contain the core purpose of the moulinette, which is **the processing to perform** on the fetched data. Quixote still provides built-ins for this phase, but the full power of the Python language and its modules can be used. Our example here shows how we can implement an inspection step that compiles the C files contained in the delivery into a program. .. code-block:: python import quixote.inspection.build.gcc as gcc @quixote.inspector def compile_delivery(): """ Compile all the C files in the student delivery to make a program. """ delivery_path = get_context()["delivery_path"] gcc(f"{delivery_path}/*.c", output_file="student") Wrap-up ------- Congratulations, you just learned the basics of Quixote! You can download the full :download:`blueprint.py ` for this tutorial. However, so far it is not that useful: it does not verify anything yet! Next, we suggest you take a look at the :doc:`validating_an_exercise` tutorial to learn about the different ways to ensure a delivery conforms to an exercise's requirements.