Quickstart: Your First Full Featured Python API

Learn how to create a user profile API in Python, package it into a container, and run it on Akka Serverless. We’ll build a system that is managing User Profiles that need to be directly incorporated into a run-time application, e.g. tracking last watched movies for a streaming video service User Profile or currently connected devices for a customer support User Profile.

Before you begin

  • If you’re new to Akka Serverless, create an account so you can try out Akka Serverless for free.

  • You’ll also need install the Akka Serverless CLI if you want to deploy from a terminal window.

  • For this quickstart, you’ll also need:

    • Docker 19.03 or higher

    • Python {minimum_python_version} or higher

    • grpcurl

No Code, Run and See

This is a quickest way to have an Akka Serverless Python API running locally. You won’t launch into coding (yet) but will have a fast path to making your first Akka Serverless-powered API call.

  1. In a terminal window, clone the repository: git clone git@github.com:jpollock/akka-serverless-getting-started-python.git;

  2. In the same terminal, navigate to the project folder: cd akka-serverless-getting-started-python;

  3. Run the containers: docker-compose up -d.

Once up and running, you can make your first API call:

curl -X POST -H "Content-Type: application/json" http://localhost:9000/hello -d '{"name": "My Name"}'

This should result in:

 {"text":"Do you want to play a game, My Name?"}

docker-compose down to shut things down.

Now, Let’s Get Setup For Developing

Before launching into actual coding, let’s finish setting up our development environment.

  1. cd akka-serverless-getting-started-python if not already there;

  2. Create a virtual environment: python3 -m venv myenv;

  3. Load the newly created environment: source myenv/bin/activate;

  4. Install the necessary Python modules: pip install -r requirements.txt

  5. Start the API: start.sh

In the terminal window you should see something like:

2021-09-11 08:53:37,698 - akkaserverless_service.py - INFO: Starting Akka Serverless on address 0.0.0.0:8080
2021-09-11 08:53:40,115 - discovery_servicer.py - INFO: discovering.
protocol_minor_version: 7
proxy_name: "akkaserverless-proxy-core"
proxy_version: "0.7.0-beta.18"
supported_entity_types: "akkaserverless.component.action.Actions"
supported_entity_types: "akkaserverless.component.view.Views"
supported_entity_types: "akkaserverless.component.valueentity.ValueEntities"
supported_entity_types: "akkaserverless.component.replicatedentity.ReplicatedEntities"
supported_entity_types: "akkaserverless.component.eventsourcedentity.EventSourcedEntities"
dev_mode: true

2021-09-11 08:53:40,115 - discovery_servicer.py - INFO: entity: com.example.MyApi
2021-09-11 08:53:40,115 - discovery_servicer.py - INFO: discovering api_spec.proto
2021-09-11 08:53:40,115 - discovery_servicer.py - INFO: SD: com.example.MyApi

Once up and running, you can make your first API call, in another terminal window:

curl -X POST -H "Content-Type: application/json" http://localhost:9000/hello -d '{"name": "My Name"}'

This should result in:

 {"text":"Do you want to play a game, My Name?"}

Building Our First API

We’re here to build something! Running the above was interesting, just to get a sense of what some aspects of the developer experience are, mainly:

To run fully in local environment, running the Akka Serverless sidecar proxy is necessary; this was the docker-compose -f docker-compose-proxy.yml up -d command above.

For the rest of this quickstart, we’ll use the specific use case: a system that is managing User Profiles that need to be directly incorporated into a run-time application, e.g. tracking last watched movies for a streaming video service User Profile or currently connected devices for a customer support User Profile and/or Chat Bot.

The objective of this first part will be to deliver on the following requirement:

We want to fetch a User Profile given the identifier for a particular user using a standard API approach.

Design and Develop the Fetch User Profile API

This API will be simple: given an identifier for a particular user, return the User Profile data. We’ll want to create an API that enables the following curl call (or accessed from wherever, either directly through gRPC or HTTP).

+

curl -X GET -H "Content-Type: application/json" http://localhost:9000/users/$\{uuidgen\}

The finished code is in the fetch_user_profile branch of this repository. git checkout fetch_user_profile to jump ahead to finished code.

Now on to actual building!

  1. Open the api_spec.proto file in your favorite code or text editor.

  2. Define the schema of the request. The data sent to the API, in this case, is quite simple: a name. This data will either be part of a JSON payload or part of the URI or request parameters. Fetching User Profile data by a person’s name wouldn’t work n the real world - too many Jeremys out there! - so let’s change name to be a parameter more like an id. Let’s change name to user_profile_id.

    message MyRequest {
        string user_profile_id = 1;
    }
  3. Now let’s move on to what data we expect to send back as a response to this request for User Profile data. text seems to not be a great choice so let’s change that to name (which nows seems appropiate!). And a name for a User Profile is rather bare bones so we will add some other attributes of a User Profile that we would like to see.

    1. Change text to name.

    2. Add string status = 2 on the line below.

    3. Add bool online = 3 on the next line.

    Your MyResponse should look like the below. Note that the numbers (1,2,3) indicate position in the returned response; the order of things matter.

    +

    message MyResponse {
       string name = 1;
       string status = 2;
       bool online = 3;
    }
  4. We have a request and a response. Now we need the API! The starter code in the file is for a POST /hello API call. We need to make some changes.

    1. Change Hello to GetUser. We’ll use this identifier in the actual code that we write to implement the API logic.

    2. Change post to get since this is a fetch of data.

    3. Remove the body: "*" line.

    4. Change /hello to /users/{user_profile_id}; we’re putting the parameter, user_profile_id, from our MyRequest message directly into the URI path.

      The finished API specification should look like:

      // boilerplate for a new action API
      syntax = "proto3";
      
      import "akkaserverless/annotations.proto";
      import "google/api/annotations.proto";
      
      
      message MyRequest {
          string user_profile_id = 1;
      }
      
      message MyResponse {
          string name = 1;
          string status = 2;
          bool online = 3;
      }
      
      service MyApi {
          rpc GetUser(MyRequest) returns (MyResponse) {
              option (google.api.http) = {
                  get: "/users/{user_profile_id}"
              };
          }
      }
  5. Generate Python stub code by running, from terminal window, in the project directory: compile.sh.

  6. Open the api_impl.py so that we can add our business logic.

  7. Change @action.unary_handler("Hello") to @action.unary_handler("GetUser"). This essentially performs the routing logic, to map the incoming API request to the URI path specified in the api_spec.proto to the function that will handle the request.

  8. Change def hello to def fetch_user. This is not necessary since we already have the routing done through the above change but it makes our code more sensible to the reader.

  9. At the top of the file, right before # imports fom Akka Serverless SDK add a new line: import random. We will just use some random data for fun.

  10. In the now named users function, change the line, resp = MyResponse(text= "Do you want to play a game, " + command.name + "?") to resp = MyResponse(name= "My Name", status= random.choice(['active', 'inactive']), online= bool(random.getrandbits(1)))

"""
Copyright 2020 Lightbend Inc.
Licensed under the Apache License, Version 2.0.
"""
import random

# imports fom Akka Serverless SDK
from akkaserverless.action_context import ActionContext
from akkaserverless.action_protocol_entity import Action

# import from generated GRPC file(s)
from api_spec_pb2 import (MyRequest, MyResponse, _MYAPI, DESCRIPTOR as API_DESCRIPTOR)


entity = Action(_MYAPI, [API_DESCRIPTOR])

@entity.unary_handler("GetUser")
def fetch_user(command: MyRequest, context: ActionContext):
    resp = MyResponse(name= "My Name", status= random.choice(['active', 'inactive']), online= bool(random.getrandbits(1)))
    return resp
  1. Start the API (and proxy) with the start.sh command in the terminal.

Make an API call through the following command:

curl -X GET -H "Content-Type: application/json" http://localhost:9000/users/$\{uuidgen\}

The result should look like:

{"name":"My Name","status":"active","online":false}%

status and online values should change randomly as you run that curl command repeatedly.

Congratulations! You have your first Akka Serverless Python API! But it was a simple one and not fully using the power of Akka Serverless. Let’s move on to creating and updating actual User Profiles.

Design and Develop the Create/Update User Profile API

The finished code is in the create_update_user branch. git checkout create_update_user to jump ahead to finished code.

So far we have built a simple API and one that has not stored any data or retrieved any either, from a database or elsewhere. For a system that is managing User Profiles that need to be directly incorporated into a run-time application, e.g. tracking last watched movies for a streaming video service User Profile or currently connected devices for a customer support User Profile.

The objective of this exercise will be to deliver on the following requirements:

We want to create and update User Profile data for a particular user using a standard API approach.
  1. Open the api_spec.proto file in your favorite code or text editor.

  2. Add a new message request, to capture the information we want to pass into the API: name of the User, status of the User, e.g. a customer or not, and list of devices.

    message UserProfile {
        string user_profile_id = 1;
        string name = 2;
        string status = 3;
        repeated Device devices = 4;
    }
    
    message Device {
        string id = 1;
        string name = 2;
    }
  3. Annotate the user_profile_id parameter in both the UserProfile and MyRequest messages such that the parameter, user_profile_id, is denoted as the identifier that Akka Serverless should use for Value Entity (KV) storage and manipulation. The annotation is: [(akkaserverless.field).entity_key = true]. See below for example on where to put.

    message UserProfile {
        string user_profile_id = 1 [(akkaserverless.field).entity_key = true];
        string name = 2;
        string status = 3;
        repeated Device devices = 4;
    }
    
    message Device {
        string id = 1;
        string name = 2;
    }
    
    message MyRequest {
        string user_profile_id = 1 [(akkaserverless.field).entity_key = true];
    }
  4. Update the MyResponse so that it can be used in this new API as well; we are just adding additional data parameters that we will include in the body of the response.

    message MyResponse {
        string name = 1;
        string status = 2;
        bool online = 3;
        repeated Device devices = 4;
    }
  5. Add the API itself. We’ll two: a POST /users for creation of a UserProfle and PUT /users/{user_profile_id} for the update.

    service MyApi {
        rpc GetUser(MyRequest) returns (MyResponse) {
            option (google.api.http) = {
                get: "/users/{user_profile_id}",
            };
        }
        rpc CreateUser(UserProfile) returns (MyResponse) {
            option (google.api.http) = {
                post: "/users",
                body: "*"
            };
        }
        rpc UpdateUser(UserProfile) returns (MyResponse) {
            option (google.api.http) = {
                put: "/users/{user_profile_id}",
                body: "*"
            };
        }
    
    }
  6. Generate code by running, from terminal window, in the project directory: compile.sh.

  7. Open the api_impl.py so that we can add our business logic. First, change the implementation of our API from an Action to a Value Entity. Replace:

    from akkaserverless.action_context import ActionContext
    from akkaserverless.action_protocol_entity import Action

    with

    from akkaserverless.value_context import ValueEntityCommandContext
    from akkaserverless.value_entity import ValueEntity
  8. Add UserProfile in front of MyRequest in the import statement:

from api_spec_pb2 import (UserProfile, MyRequest, MyResponse, _MYAPI, DESCRIPTOR as API_DESCRIPTOR)
  1. Change the API implementation type from Action to ValueEntity. Replace::

    entity = Action(_MYAPI, [API_DESCRIPTOR])

    with

    entity = ValueEntity(_MYAPI, [API_DESCRIPTOR])
  2. Initialize default data for the User Profile. After the import statements, add:

def init(entity_id: str) -> UserProfile:
    return UserProfile()
  1. Update the entity definition to include both the type and the default data:

entity = ValueEntity(_MYAPI, [API_DESCRIPTOR], 'user_profiles', init)
  1. Since we’ve moved from Action to ValueEntity we need to change some things in our GetUser API code.

    1. Change unary_handler to command_handler.

    2. Change command: MyRequest, context: ActionContext to state: UserProfile, command: MyRequest, context: ValueEntityCommandContext.

    3. The api specification calls for a response of type MyResponse. Change

      resp = MyResponse(name= "My Name", status= random.choice(['active', 'inactive']), online= bool(random.getrandbits(1)))
      return resp

      to

      resp = MyResponse(name= state.name, status= state.status, online= bool(random.getrandbits(1)))
      return resp
  2. Add a new command handler for CreateUser.

    1. Add a new line after the fetch_user function: @entity.command_handler("CreateUser").

    2. Define a function, called create_user with parameters of state, command, context. The Akka Serverless Python SDK depends on these exact names.

    3. Your code should look like:

      @entity.command_handler("CreateUser")
      def create_user(state: UserProfile, command: UserProfile, context: ValueEntityCommandContext):
         state = command
         context.update_state(state)
         return MyResponse(name= state.name, status= state.status, online= bool(random.getrandbits(1)))
  3. Add a new command handler for UpdateUser.

    1. Add a new line after the create_user function: @entity.command_handler("UpdateUser").

    2. Define a function, called update_user with parameters of state, command, context.

    3. Let’s make it a bit more complex and do some basic testing. The idea is the same: select which data off of the command (request) that you want to map into the state.

      @entity.command_handler("UpdateUser")
      def update_user(state: UserProfile, command: UserProfile, context: ValueEntityCommandContext):
          if command.name and command.name != state.name:
              state.name = command.name
          if command.status and command.status != state.status:
              state.status = command.status
          context.update_state(state)
          return MyResponse(name= state.name, status= state.status, online= bool(random.getrandbits(1)))
  4. Now let’s start our updated API: start.sh

curl -X POST -H "Content-Type: application/json" http://localhost:9000/users -d '{"user_profile_id": "test", "name": "My Name", "status": "active", "devices":[]}'

curl -X GET -H "Content-Type: application/json" http://localhost:9000/users/test

curl -X PUT -H "Content-Type: application/json" http://localhost:9000/users/test -d '{"status": "inactive"}'

Design the User Profile Query API

So far we have built the API endpoints for creating, updating and fetching User Profile data. Let’s continue our development by using the Views capability of Akka Serverless to create queryable data.

The finished code is in the query_users branch. git checkout query_users to jump ahead to finished code.

First, we have to know that Akka Serverless users (CQRS)][https://martinfowler.com/bliki/CQRS.html] for supporting querying capabilities; the query mechanism always resides in a separate service/API, even if running within the same container environment. This ultimately gives the developer more control and performance for querying, whether for basic or more advanced needs. THis means that we need to add a peer service definition. Normally, we’re likely to do this in a separate file and probably repository as well. But for now we’ll do in the same API specification file: api_spec.proto.
  1. Open up proto file

  2. Add an empty service definition at the end of the file, called MyQueryApi:

    service MyQueryApi {
    
    }
  3. Querying in Akka Serverless is based on a feature called (views)[https://developer.lightbend.com/docs/akka-serverless/reference/glossary.html#view]. Views are created automatically by Akka Serverless based on the events that occur, e.g. new data created, updated or deleted. As such, we have to define an API - not called directly by a developer - that will be used by Akka Serverless to connect the events to the query tables that will be created and managed. We do this using the eventing annotation. We will add this API endpoint to the MyQueryApi.

    service MyQueryApi {
        rpc UpdateView(UserProfile) returns (UserProfile) {
            option (akkaserverless.method).eventing = {
                in: {
                    value_entity: "user_profiles"
                }
            };
            option (akkaserverless.method).view.update = {
                table: "user_profiles"
            };
        }
    }
  4. The second part of our new MyQueryApi will be the actual API call that can be used by a developer to make the actual query. In this case, let’s do a simple query for fetching all of the users created by our MyApi. Name of the API is GetUsers, with no input parameters supported an a resonse of type UsersResponse. The annotation used, to define the query, is option (akkaserverless.method).view.query and the query is a simple select statement: SELECT * AS results FROM user_profiles.

    rpc GetUsers(google.protobuf.Empty) returns (UsersResponse) {
        option (akkaserverless.method).view.query = {
        query: "SELECT * AS results FROM user_profiles"
        };
        option (google.api.http) = {
            get: "/users"
        };
    }
  5. In the above, we have a new message type that we have to define: UsersResponse. In this case, it is a simple list of UserProfile messages. The definition is:

    message UsersResponse {
        repeated UserProfile results = 1;
    }
  6. Compile the stub code based on our updated protobuf:

    compile.sh
  7. Now we have our API defined and the eventing setup so the underlying data structures are populated. We connect this to code by updating our api_impl.py. We need to import some modules and then setup the View. You can add the below right after the from api_spec_pb2 import (UserProfile, MyRequest, MyResponse, _MYAPI, DESCRIPTOR as API_DESCRIPTOR) line. You could also put at the bottom of the file as well.

    from akkaserverless.view import View
    from api_spec_pb2 import (_MYQUERYAPI, DESCRIPTOR as FILE_DESCRIPTOR)
    
    view = View(_MYQUERYAPI,[FILE_DESCRIPTOR])
  8. The final step is to expose it to the Akka Serverless run-time. Update the api_service.py file to import the view and add it to the list of components served. The file should look like:

    """
    Copyright 2020 Lightbend Inc.
    Licensed under the Apache License, Version 2.0.
    """
    
    from akkaserverless.akkaserverless_service import AkkaServerlessService
    from api_impl import entity as myapi
    from api_impl import view as myquery
    import logging
    
    if __name__ == '__main__':
        logging.basicConfig(level=logging.DEBUG)
    
        # create service and add components
        service = AkkaServerlessService()
        service.add_component(myapi)
        service.add_component(myquery)
        service.start()
  9. Start the API service again: start.sh

curl -X POST -H "Content-Type: application/json" http://localhost:9000/users -d '{"user_profile_id": "test", "name": "My Name", "status": "active", "devices":[]}'

curl -X POST -H "Content-Type: application/json" http://localhost:9000/users -d '{"user_profile_id": "test2", "name": "My Name", "status": "active", "devices":[]}'

curl -X GET -H "Content-Type: application/json" http://localhost:9000/users

Package and deploy your service

To compile, build the container image, and publish it to your container registry, follow these steps

  1. Set your DOCKER_REGISTRY and DOCKER_USER environment variables:

    export DOCKER_REGISTRY=<your Docker registry, e.g. docker.io>#
    export DOCKER_USER=<your Docker registry username>
  2. From the root project directory, build the docker container (the 0.0.1 is just the version number you want to use to tag the container):

    docker_build.sh 0.0.1
  3. Push the built container to the registry:

    docker_push.sh 0.0.1
  4. Sign in to your Akka Serverless account at: Akka Serverless Console

  5. If you do not have a project, click Add Project to create one, otherwise choose the project you want to deploy your service to.

  6. On the project dashboard click the "+" next to services to start the deployment wizard

  7. Choose a name for your service and click Next

  8. Enter the container image URL from the above step and click Next

  9. Click Next (no environment variables are needed for these samples)

  10. Check both Add a route to this service and Enable CORS and click Next

  11. Click Finish to start the deployment

  12. Click Go to Service to see your newly deployed service

Invoke your service

Now that you have deployed your service, the next step is to invoke it using gRPCurl

  1. From the "Service Explorer" click on the method you want to invoke

  2. Click on "gRPCurl"

  3. In the bottom section of the dialog, fill in the values you want to send to your service

  4. In the top section of the dialog, click the "Copy to clipboard" button

  5. Open a new command line and paste the content you just copied