Define Task class inputs
========================
There are two ways of defining task inputs in Ewoks when using the `Task` class:
- :ref:`Input names`: inputs are defined by their names, given as lists of strings
- :ref:`Input model`: inputs are defined by a `Pydantic model `_
These two methods are incompatible and only one should be picked (see :ref:`Incompatibility`).
.. _Input names:
Input names
-----------
Required input names are given as a list of strings via the ``input_names`` subclass argument of ``Task``.
They can then be retrieved in the task via ``self.inputs`` or ``self.get_input_value``:
.. code-block:: python
from ewoks import Task
class ExampleTask(Task, input_names=["a", "b"]):
def run(self):
a = self.inputs.a
b = self.get_input_value("b")
print(f"a={a} b={b}")
For demonstration purposes, we can `execute a task from Python <./task_python.rst>`_.
.. code-block:: python-console
>>> task = ExampleTask(inputs={"a": 1, "b": 2})
>>> task.run()
a=1 b=2
Since these are **required** input names, Ewoks will throw a ``TaskInputError`` if one is missing
.. code-block:: python-console
>>> task = ExampleTask(inputs={"a": 1})
ewokscore.task.TaskInputError: Missing inputs for : {'b'}
We can allow optional inputs by giving a list of strings to a ``optional_input_names`` subclass argument of ``Task``
.. code-block:: python
class ExampleTaskWithOptional(Task, input_names=["a", "b"], optional_input_names=["c"]):
def run(self):
a = self.inputs.a
b = self.get_input_value("b")
c = self.inputs.c
print(f"a={a} b={b} c={c}")
These optional inputs can be specified
.. code-block:: python-console
>>> task = ExampleTaskWithOptional(inputs={"a": 1, "b": 2, "c": 3})
>>> task.run()
a=1 b=2 c=3
or omitted. In this case, they will be set to the special object ``MISSING_DATA``:
.. code-block:: python-console
>>> task = ExampleTaskWithOptional(inputs={"a": 1, "b": 2})
>>> task.run()
a=1 b=2 c=
Default values can be given thanks to ``get_input_value``
.. code-block:: python
class ExampleTaskWithDefault(Task, input_names=["a", "b"], optional_input_names=["c"]):
def run(self):
a = self.inputs.a
b = self.get_input_value("b")
c = self.get_input_value("c", 0) # <-- Default value
print(f"a={a} b={b} c={c}")
.. code-block:: python-console
>>> task = ExampleTaskWithDefault(inputs={"a": 1, "b": 2})
>>> task.run()
a=1 b=2 c=0
Subclassing
^^^^^^^^^^^
Subclasses of a task will inherit the input names from the base class and any additional input name will be added to those:
.. code-block:: python
class ChildExampleTask(ExampleTaskWithOptional, input_names=["d"]):
def run(self):
print(self.get_input_values())
.. code-block:: python-console
>>> task = ChildExampleTask(inputs={"a": 1, "b": 2, "c": 3, "d": 4})
>>> # Accepts `a`, `b` and `c` in addition to `d` ^
>>> task.run()
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
.. _Input model:
Input model
-----------
.. versionadded:: 1.1.0
Instead of input names, it is possible to provide a `Pydantic model `_ for inputs.
The model needs to derive from ``ewoks.BaseInputModel`` and must be provided via ``input_model`` to the task.
Inputs can then be retrieved in the task via ``self.inputs`` or ``self.get_input_value``:
.. code-block:: python
from ewoks import Task
from ewoks import BaseInputModel
class Inputs(BaseInputModel):
a: int
b: int
class ExampleTaskWithModel(Task, input_model=Inputs):
def run(self):
a = self.inputs.a
b = self.get_input_value("b")
print(f"a={a} b={b}")
.. code-block:: python-console
>>> task = ExampleTaskWithModel(inputs={"a": 1, "b": 2})
>>> task.run()
a=1 b=2
The advantage of using a model is that **inputs are validated by Pydantic**
.. code-block:: python-console
>>> task = ExampleTaskWithModel(inputs={"a": 1})
ewokscore.task.TaskInputError: 1 validation error for Inputs
b
Field required [type=missing, input_value={'a': 1}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.10/v/missing
>>> task = ExampleTaskWithModel(inputs={"a": 1, "b": "not_a_number"})
ewokscore.task.TaskInputError: 1 validation error for Inputs
b
Input should be a valid integer, unable to parse string as an integer [type=int_parsing, input_value='not_a_number', input_type=str]
For further information visit https://errors.pydantic.dev/2.10/v/int_parsing
All Pydantic features such has `Default values `_, `Constraints `_ or `Custom validation `_ are available.
For example, we can add an optional input to our model by giving it a default value
.. code-block:: python
from ewoks import Task
from ewoks import BaseInputModel
class Inputs(BaseInputModel):
a: int
b: int
c: int = 0 # <-- `c` is optional
class ExampleTaskWithModel(Task, input_model=Inputs):
def run(self):
a = self.inputs.a
b = self.get_input_value("b")
c = self.inputs.c
print(f"a={a} b={b} c={c}")
.. code-block:: python-console
>>> task = ExampleTaskWithModel(inputs={"a": 1, "b": 2})
>>> task.run()
a=1 b=2 c=0
.. note::
It is possible to reproduce the default behaviour of `optional_input_names` that are set to ``MISSING_DATA`` if not set.
For this, add ``MissingData`` as a possible type for the input in the model and use ``MISSING_DATA`` as the default value
.. code-block:: python
from ewoks import Task
from ewoks import BaseInputModel
from ewokscore.missing_data import MissingData, MISSING_DATA
class Inputs(BaseInputModel):
a: int
b: int
c: int | MissingData = MISSING_DATA # <-- `c` is optional
class ExampleTaskWithModel(Task, input_model=Inputs):
def run(self):
a = self.inputs.a
b = self.get_input_value("b")
c = self.inputs.c
print(f"a={a} b={b} c={c}")
.. code-block:: python-console
>>> task = ExampleTaskWithModel(inputs={"a": 1, "b": 2})
>>> task.run()
a=1 b=2 c=
.. _Subclassing models:
Subclassing
^^^^^^^^^^^
Subclasses of tasks with input models will inherit the model if they do not implement a model themselves
.. code-block:: python
class ChildTask(ExampleTaskWithModel):
def run(self):
print(self.get_input_values())
.. code-block:: python-console
>>> task = ChildTask(inputs={"a": 1, "b": 2, "c": 3})
>>> task.run()
{'a': 1, 'b': 2, 'c': 3}
If the subclass does have an input model, it must be inherit from the model of the base class to have compliant inputs
.. code-block:: python
class NewInputs(Inputs):
d: int
class ChildTaskWithModel(ExampleTaskWithModel, input_model=NewInputs):
def run(self):
print(self.get_input_values())
.. code-block:: python-console
>>> task = ChildTaskWithModel(inputs={"a": 1, "b": 2, "c": 3, "d": 4})
>>> task.run()
{'a': 1, 'b': 2, 'c': 3, 'd': 4}
.. _Incompatibility:
Incompatibility between methods
-------------------------------
.. danger::
It is **not possible to mix** ``input_names``/``optional_input_names`` and ``input_model``!
This means a ``Task`` **cannot** have both ``input_names``/``optional_input_names`` and ``input_model`` defined.
But also, **a subclass must use the same input definition method as its base class**:
- If the base class task uses ``input_names``/``optional_input_names``, the subclass must use ``input_names``/``optional_input_names`` as well.
- If the base class task uses ``input_model``, the subclass must use an ``input_model`` that subclasses the model of the base task (see :ref:`above `).