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

pip install nomnomdata-tools-engine nomnomdata-engine

Create a new app template directory structure, similar to create-react-app

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

engine/Dockerfile

engine/requirements.txt

engine/pkg/__init__.py

engine/pkg/executable.py

engine/pkg/tests/__init__.py

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

# 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

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

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 API Reference

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:

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 Engine for more details

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.

  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.

@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

@engine.action(display_name="An Action", description="Action description")

The top level decorator here is action() . It defines our function as an action, gives it a name and also a description. Lets add a reference to a help file.

@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 Parameter, ParameterGroup and decorating our action with engine. parameter_group()

@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

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.

def an_action(parameters):
      engine.update_progress(progress=50)
      print(parameters)

What does parameters look like in this case?

{
    "until" : "<value the user sets in the interface>"
}

Next we ensure we call main() as this is the entry point of every engine

if __name__ == "__main__":
      engine.main()

Running tests locally

Looking at 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!

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 ExecutionContextMock to wrap your code like so.

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 my.nomnomdata.com .

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.

python pkg/executable.py dump-yaml

Publish the engine for all the Nominodes assigned to your organization.

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.

nnd engine-tools deploy -n nomitall-prod

Full CLI Reference