Prerequisites ============= ``Python >= 3.7.7 <3.8`` ``Docker >= 19.03`` ``pip >= 20.1.1`` .. note:: We have a much more in depth `getting started guide `_ for anyone wants it Quick-start =========== Install from PyPi ----------------- .. code-block:: bash pip install nomnomdata-tools-engine nomnomdata-engine Create a new app template directory structure, similar to ``create-react-app`` .. code-block:: bash nnd engine-tools create-new waiter-app Template Layout --------------- These files and folders will get created locally on your machine to form the structure of your NND App :ref:`engine/Dockerfile` :ref:`engine/requirements.txt` engine/pkg/__init__.py :ref:`engine/pkg/executable.py` engine/pkg/tests/__init__.py :ref:`engine/pkg/tests/test_executable.py` engine/requirements.txt ^^^^^^^^^^^^^^^^^^^^^^^ Pip `requirements file `_ . Initially blank. engine/Dockerfile ^^^^^^^^^^^^^^^^^ Template Dockerfile, we've done a little work here for you to utilize `multi-stage builds `_ and install your requirements.txt. The important thing is to set CMD to run your engine entry point. .. note:: If you are unfamilar with Docker/dockerfiles it would be advantageous to familarize yourself over at the `docker documentation `_ .. code-block:: docker # Starting from an image of our SDK as the initial build layer FROM registry.gitlab.com/nomnomdata/tools/nomnomdata-engine:latest as builder # Copy your requirements.txt file into the build layer environmens # use its contents to create pip wheels for installing # the packages and versions specified COPY requirements.txt /nomnom/requirements.txt RUN pip wheel --wheel-dir /python_packages -r /nomnom/requirements.txt # Use our 'slim' image as the base for the next layer # which will be the layer that is deployed FROM registry.gitlab.com/nomnomdata/tools/nomnomdata-engine:latest-slim # Copy the python wheels from our previous builder layer COPY --from=builder /python_packages /python_packages # Install the packages and remove the # temporary /python_packages directory RUN pip install --no-index --find-links /python_packages /python_packages/* \ && rm -rf /python_packages # Set up /nomnom as the working directory # and copy every file in the pkg directory (your app code) into it WORKDIR /nomnom/ COPY pkg/*.* /nomnom/pkg/ # sets the command that will run # your code inside the container when # it is triggered from a Nominode Task: CMD python pkg/executable.py run engine/pkg/executable.py ^^^^^^^^^^^^^^^^^^^^^^^^ Main entry module of the example app. engine/pkg/tests/test_executable.py ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Contains an extremely simple `pytest `_ compatible test for our new app. Understanding executable.py --------------------------- Our executable.py looks like this .. code-block:: python import logging from nomnomdata.engine import Engine, Parameter, ParameterGroup from nomnomdata.engine.parameters import Int, String logger = logging.getLogger("nnd.some-cool-engine") engine = Engine( uuid="CHANGE-ME-PLEASE", alias="Some Cool Engine", description="Description of your engine", categories=["general"], ) @engine.action(display_name="An Action", description="Action description") @engine.parameter_group( ParameterGroup( Parameter( type=Int(), name="integer_parameter", display_name="An Integer Parameter", description="Description of what the parameter is used for", ), Parameter( type=String(), name="string_parameter", display_name="A String Parameter", description="Description of what the parameter is used for", ), name="general_parameters", display_name="General Parameters", description="Description of the parameter group", ) ) def an_action(parameters): engine.update_progress(progress=50) print(parameters) if __name__ == "__main__": engine.main() Lets break it down .. code-block:: python import logging We're using the standard library `logging module `_ .. note:: When your app runs via ``python pkg.py run``, a special `log handler `_ is attached to the root logger. This handler sends all log messages to the nominode for storage and display in the UI Next import the building blocks of an engine from ``nomnomdata.engine``, information here :ref:`API Reference` .. code-block:: python :emphasize-lines: 1 from nomnomdata.engine import Engine, Parameter, ParameterGroup from nomnomdata.engine.parameters import Int, String Instantiate our logger to be used later, the name ``nnd.some-cool-engine`` here is an example, rename it whatever suits your App best: .. code-block:: python logger = logging.getLogger("nnd.some-cool-engine") These lines define the unique identifier, the display name, the description and the categories that apply to the NND App: You'll want to change these to be more appropriate. Think of the UUID as a url slug. You can consult the reference docs for :class:`~nomnomdata.engine.components.Engine` for more details .. code-block:: python engine = Engine( uuid="CHANGE-ME-PLEASE", alias="Some Cool Engine", description="Description of your engine", categories=["general"], ) .. warning:: The UUID must be globally unique, and cannot collide with any existing app UUID owned by anyone on the platforms If we want to have some icons display for the App on the nominode, it's a simple change. .. code-block:: python :emphasize-lines: 6-10 engine = Engine( uuid="TUTOR-WAITR", alias="Waiter Tutorial", description="Example of an NND App that waits until a time specified.", categories=["tutorial","wait"], icons={ "1x": "../../icons/icon-1x.png", "2x": "../../icons/icon-2x.png", "3x": "../../icons/icon-3x.png", }, ) .. note:: Icon paths are relative to where you run ``python executable.py dump-yaml`` from. Next we come to the definition of the action this engine can perform. .. code-block:: python @engine.action(display_name="An Action", description="Action description") @engine.parameter_group( ParameterGroup( Parameter( type=Int(), name="integer_parameter", display_name="An Integer Parameter", description="Description of what the parameter is used for", ), Parameter( type=String(), name="string_parameter", display_name="A String Parameter", description="Description of what the parameter is used for", ), name="general_parameters", display_name="General Parameters", description="Description of the parameter group", ) ) def an_action(parameters): engine.update_progress(progress=50) print(parameters) There's a lot going on here so let's break it down further .. code-block:: python @engine.action(display_name="An Action", description="Action description") The top level decorator here is :meth:`~nomnomdata.engine.components.Engine.action` . It defines our function as an action, gives it a name and also a description. Lets add a reference to a help file. .. code-block:: python :emphasize-lines: 4 @engine.action( display_name="An Action", description="Action Description", help_md_path="../../help/waiter-tutorial-wait_until.md" ) .. note:: ``help_md_path`` is relative to where you run ``python executable.py dump-yaml`` from. Next up we're defining what parameters we want our action to accept, using :class:`~nomnomdata.engine.components.Parameter`, :class:`~nomnomdata.engine.components.ParameterGroup` and decorating our action with engine. :meth:`~nomnomdata.engine.components.Engine.parameter_group` .. code-block:: python @engine.parameter_group( ParameterGroup( Parameter( type=Int(), name="integer_parameter", display_name="An Integer Parameter", description="Description of what the parameter is used for", ), Parameter( type=String(), name="string_parameter", display_name="A String Parameter", description="Description of what the parameter is used for", ), name="general_parameters", display_name="General Parameters", description="Description of the parameter group", ) ) .. note:: :meth:`~nomnomdata.engine.components.Engine.parameter_group` is not limited to just accepting one Parameter, you can pass as many parameters as you want as positional arguments Next we have our actual function. We use our engine instance to communicate our progress back to the nominode. .. code-block:: python def an_action(parameters): engine.update_progress(progress=50) print(parameters) What does parameters look like in this case? .. code-block:: python { "until" : "" } Next we ensure we call :meth:`~nomnomdata.engine.components.Engine.main` as this is the entry point of every engine .. code-block:: python if __name__ == "__main__": engine.main() Running tests locally --------------------- Looking at :ref:`engine/pkg/tests/test_executable.py` we can see just how easy it is to test things locally, when you call an action in any code (outside of the run cli command), we mock up a nominode for your code to "talk" to! .. code-block:: python from ..executable import an_action def test_an_action(): an_action(integer_parameters=1, string_parameters="some_string") .. warning:: If you use any nominode communication functions outside the context of an action entry point, you will have have to use :class:`~nomnomdata.engine.components.engine.ExecutionContextMock` to wrap your code like so. .. code-block:: python with ExecutionContextMock(): # code that uses NominodeClient pass Deploying your App ------------------ If you have not yet run ``nnd login`` now is the time. Use your credentials for ``_ . .. note:: Engines will be deployed to the organization you select and only be available to nominodes under that organization All commands must be run in the `engine` folder of your template app Generate a model.yaml file for your NND App. .. code-block:: bash python pkg/executable.py dump-yaml Publish the engine for all the Nominodes assigned to your organization. .. code-block:: bash nnd engine-tools model-update -n nomitall-prod Build the local docker image for your NND App and upload it to our secure image repository. .. code-block:: bash nnd engine-tools deploy -n nomitall-prod Full :doc:`cli-reference`