Anatomy of a Service

In this tutorial we will go through the code of the service that is generated by daeploy init to learn the parts that make up a Daeploy service.

>>> daeploy init 
project_name [my_project]: my_first_daeploy_project
>>> ls ./my_first_daeploy_project -a 
.  ..  .s2i/  .s2iignore  README.md  requirements.txt  service.py  tests/

service.py contains the service code, requirements.txt contains any dependencies of service.py and .s2i/ contains a file that defines environment variables. To create a service with the Daeploy SDK, only these three files are strictly required. The remaining files serve other purposes and we touch on them in later tutorials.

Let’s take a look at the contents of service.py to see how the SDK is used to create the service.

Setup

The first step is to import packages. We also initialize a logger so we can get information about the service for debugging:

import logging
from daeploy import service
from daeploy.communication import Severity, notify

logger = logging.getLogger(__name__)

Only service is actually required, but we recommend to import and use the standard python logging package. notify() and Severity in the daeploy.communication module are used for the notification functionality in Daeploy.

If any external packages are used, they must be specified in the requirements.txt file. That way they will be installed the service is deployed.

Note

It is highly recommended to pin your requirements to specific versions when in a production environment, for example numpy==1.19.4

The service object helps us set up entrypoints for the service, add parameters and start the service. The logger is just a regular python logging object, which Daeploy natively supports. The logs from a service can be read from the dashboard or using daeploy logs name version.

Defining parameters

It is possible to define parameters that automatically get exposed to the API and can be freely changed from outside a running service

service.add_parameter("greeting_phrase", "Hello")

Get the value of a parameter with

greeting_phrase = service.get_parameter("greeting_phrase")

This way you can control the behaviour of your running services without having to make any code changes. We recommend using them for control parameters.

Creating an Entrypoint

To define an entrypoint for a service we use the entrypoint decorator

@service.entrypoint
def hello(name: str) -> str:
    greeting_phrase = service.get_parameter("greeting_phrase")
    logger.info(f"Greeting someone with the name: {name}")
    return f"{greeting_phrase} {name}"

This will automatically expose the hello() function to the API. We strongly recommend that you use type hints in your Daeploy entrypoint functions. That way, you will get type verification in your API and the auto-generated documentation will show the expected data types. Please take a look at Typing in the SDK for a more detailed guide on how typing is handled in Daeploy.

Note

Daeploy entrypoints should have JSON-compatible data as input and output. Note that e.g. numpy.ndarray and pandas.DataFrame are not JSON-compatible and must be converted to lists or dictionaries. Read Using Non-jsonable Data Types on how to use such data types.

Notifications

Notifications is another feature of Daeploy. When a notification is raised it can be viewed on the dashboard at http://your-host or sent to an email address. To add a notification we use the notify() function. They can be placed in a conditional statement to send a notification if it’s true.

Let’s say that we want to be notified if someone is greeting the world, because that would take a lot of time. Then we add an if-statement to check the input and then send the notification. With this added, the hello() function now looks like this:

@service.entrypoint
def hello(name: str) -> str:
    greeting_phrase = service.get_parameter("greeting_phrase")
    if name == "World":
        notify(
            msg="Someone is trying to greet the World, too time consuming. Skipping!",
            severity=Severity.WARNING,
        )
        return "Greeting failed"
    logger.info(f"Greeting someone with the name: {name}")
    return f"{greeting_phrase} {name}"

Starting the Service

The last thing we have to do is to ensure the service runs once it is deployed

if __name__ == '__main__':
    service.run()

Full Code

All together the full service contains fewer than 25 lines of code, including input validation, logging, notifications and configurable parameters:

import logging
from daeploy import service
from daeploy.communication import Severity, notify

logger = logging.getLogger(__name__)

service.add_parameter("greeting_phrase", "Hello")

@service.entrypoint
def hello(name: str) -> str:
    greeting_phrase = service.get_parameter("greeting_phrase")
    if name == "World":
        notify(
            msg="Someone is trying to greet the World, too time consuming. Skipping!",
            severity=Severity.WARNING,
        )
        return "Greeting failed"
    logger.info(f"Greeting someone with the name: {name}")
    return f"{greeting_phrase} {name}"


if __name__ == '__main__':
    service.run()

Deploying the Service

With the service code in place we can deploy it with:

>>> daeploy deploy hello 1.0.0 ./my_first_daeploy_project/ 
Deploying service...
Service deployed successfully
MAIN    NAME    VERSION    STATUS    RUNNING
------  ------  ---------  --------  -----------------------------------
*       hello   1.0.0      running   Running (since 2020-11-23 10:29:01)

What’s Next?

Now you have seen the different components of the SDK and you should be ready to create your own service. The next step could be to take a look at the manager Dashboard, or the Software Development Kit documentation.