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:
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.
-
In a terminal window, clone the repository:
git clone git@github.com:jpollock/akka-serverless-getting-started-python.git
; -
In the same terminal, navigate to the project folder:
cd akka-serverless-getting-started-python
; -
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.
-
cd akka-serverless-getting-started-python
if not already there; -
Create a virtual environment:
python3 -m venv myenv
; -
Load the newly created environment:
source myenv/bin/activate
; -
Install the necessary Python modules:
pip install -r requirements.txt
-
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:
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:
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 |
Now on to actual building!
-
Open the
api_spec.proto
file in your favorite code or text editor. -
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 anid
. Let’s changename
touser_profile_id
.message MyRequest { string user_profile_id = 1; }
-
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 toname
(which nows seems appropiate!). And aname
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.-
Change
text
toname
. -
Add
string status = 2
on the line below. -
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; }
-
-
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.-
Change
Hello
toGetUser
. We’ll use this identifier in the actual code that we write to implement the API logic. -
Change
post
toget
since this is a fetch of data. -
Remove the
body: "*"
line. -
Change
/hello
to/users/{user_profile_id}
; we’re putting the parameter,user_profile_id
, from ourMyRequest
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}" }; } }
-
-
Generate Python stub code by running, from terminal window, in the project directory:
compile.sh
. -
Open the
api_impl.py
so that we can add our business logic. -
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 theapi_spec.proto
to the function that will handle the request. -
Change
def hello
todef 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. -
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. -
In the now named
users
function, change the line,resp = MyResponse(text= "Do you want to play a game, " + command.name + "?")
toresp = 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
-
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 |
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:
-
Open the
api_spec.proto
file in your favorite code or text editor. -
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; }
-
Annotate the
user_profile_id
parameter in both theUserProfile
andMyRequest
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]; }
-
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; }
-
Add the API itself. We’ll two: a
POST /users
for creation of a UserProfle andPUT /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: "*" }; } }
-
Generate code by running, from terminal window, in the project directory:
compile.sh
. -
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
-
Add
UserProfile
in front ofMyRequest
in the import statement:
from api_spec_pb2 import (UserProfile, MyRequest, MyResponse, _MYAPI, DESCRIPTOR as API_DESCRIPTOR)
-
Change the API implementation type from
Action
toValueEntity
. Replace::entity = Action(_MYAPI, [API_DESCRIPTOR])
with
entity = ValueEntity(_MYAPI, [API_DESCRIPTOR])
-
Initialize default data for the User Profile. After the import statements, add:
def init(entity_id: str) -> UserProfile: return UserProfile()
-
Update the entity definition to include both the
type
and the default data:
entity = ValueEntity(_MYAPI, [API_DESCRIPTOR], 'user_profiles', init)
-
Since we’ve moved from
Action
toValueEntity
we need to change some things in ourGetUser
API code.-
Change
unary_handler
tocommand_handler
. -
Change
command: MyRequest, context: ActionContext
tostate: UserProfile, command: MyRequest, context: ValueEntityCommandContext
. -
The api specification calls for a response of type
MyResponse
. Changeresp = 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
-
-
Add a new command handler for
CreateUser
.-
Add a new line after the
fetch_user
function:@entity.command_handler("CreateUser")
. -
Define a function, called
create_user
with parameters ofstate
,command
,context
. The Akka Serverless Python SDK depends on these exact names. -
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)))
-
-
Add a new command handler for
UpdateUser
.-
Add a new line after the
create_user
function:@entity.command_handler("UpdateUser")
. -
Define a function, called
update_user
with parameters ofstate
,command
,context
. -
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)))
-
-
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 |
-
Open up proto file
-
Add an empty service definition at the end of the file, called
MyQueryApi
:service MyQueryApi { }
-
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 theeventing
annotation. We will add this API endpoint to theMyQueryApi
.service MyQueryApi { rpc UpdateView(UserProfile) returns (UserProfile) { option (akkaserverless.method).eventing = { in: { value_entity: "user_profiles" } }; option (akkaserverless.method).view.update = { table: "user_profiles" }; } }
-
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 ourMyApi
. Name of the API isGetUsers
, with no input parameters supported an a resonse of typeUsersResponse
. The annotation used, to define the query, isoption (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" }; }
-
In the above, we have a new message type that we have to define:
UsersResponse
. In this case, it is a simple list ofUserProfile
messages. The definition is:message UsersResponse { repeated UserProfile results = 1; }
-
Compile the stub code based on our updated protobuf:
compile.sh
-
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 thefrom 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])
-
The final step is to expose it to the Akka Serverless run-time. Update the
api_service.py
file to import theview
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()
-
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
-
Set your
DOCKER_REGISTRY
andDOCKER_USER
environment variables:export DOCKER_REGISTRY=<your Docker registry, e.g. docker.io># export DOCKER_USER=<your Docker registry username>
-
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
-
Push the built container to the registry:
docker_push.sh 0.0.1
-
Sign in to your Akka Serverless account at: Akka Serverless Console
-
If you do not have a project, click Add Project to create one, otherwise choose the project you want to deploy your service to.
-
On the project dashboard click the "+" next to services to start the deployment wizard
-
Choose a name for your service and click Next
-
Enter the container image URL from the above step and click Next
-
Click Next (no environment variables are needed for these samples)
-
Check both Add a route to this service and Enable CORS and click Next
-
Click Finish to start the deployment
-
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
-
From the "Service Explorer" click on the method you want to invoke
-
Click on "gRPCurl"
-
In the bottom section of the dialog, fill in the values you want to send to your service
-
In the top section of the dialog, click the "Copy to clipboard" button
-
Open a new command line and paste the content you just copied