Walkthrough
Contents
Walkthrough#
Table of Contents
Introduction#
The PyCape SDK helps users write their Cape functions in Python, and allows users to run functions which have already been deployed to Cape. Users call these functions from Python using either Cape.run or Cape.invoke. The PyCape SDK also provides some extra utilities for Python users to prepare their functions for the enclave.
Prior to following this walkthrough, we encourage the reader to set up their local environment by following the Quick Start guide.
In this guide, we will walk through several examples of writing a Cape function and invoking it from Python.
A Simple Example#
Let’s start by writing a simple function. In this example, we’ll take the average, or mean, of a list of numbers. Cape expects a cape_handler
function that takes a single parameter of bytes as input. We refer to this as a “Cape function”.
Here’s a Cape function that computes the mean of a list of numbers:
import json
import statistics
def cape_handler(x_bytes):
x = json.loads(x_bytes.decode())
x_mean = statistics.mean(x)
return json.dumps(x_mean).encode()
Deploying our first function#
To deploy this function, we can follow two steps.
First, save the code into an app.py
file in a folder called mean
, similar to the “Writing Functions” guide. The result should look like this example.
Then deploy this folder by calling cape deploy mean
, per the “Deploying Functions” guide.
Running our first function#
Once we’ve deployed the Cape function successfully, we’ll have a function ID and checksum. The next step is to generate a personal access token for your account:
$ cape token create --name walkthrough --expiry 300s
Success! Your token: eyJhtGckO12...(token omitted)
You can save the token in the output to a file called walkthrough.token
. We can now read from this token file when running the function from PyCape
function_id: str = "<REPLACE THIS WITH FUNCTION ID FROM DEPLOY OUTPUT>"
function_checksum: str = "<REPLACE WITH CHECKSUM FROM DEPLOY OUTPUT>"
token_file = "walkthrough.token"
cape = Cape()
f = cape.function(function_id, checksum=function_checksum)
t = cape.token(token_file)
x_bytes = json.dumps([1, 2, 3, 4]).encode()
result_bytes = cape.run(f, t, x_bytes)
print("Mean of x is:", json.loads(result_bytes.decode()))
# Mean of x is: 2.5
Congrats! We’ve called our first Cape function from PyCape.
Mean v2: Running functions on Python types with Serdio#
The previous example shows how to run a single-parameter function mapping bytes to bytes. However, handling serialization and deserialization of arbitrary inputs for every Cape function can lead to a lot of boilerplate. We can use Serdio to automate this for Cape functions with arbitrary signatures.
Serdio, or SERialization and Deserialization of (function) Inputs and Outputs, is a small library that simplifies the Cape function development process. Serdio implements automatic serialization and deserialization of native Python types with MessagePack, with additional support for custom types via user-supplied helpers.
To use Serdio, we decorate our cape handler function with @serdio.lift_io
and set use_serdio=True
when we call Cape.run
or Cape.invoke
. See examples/mean/app.py for instructions on decorating.
Let’s rewrite the previous averaging example in app.py
to use Serdio:
import statistics
import serdio
@serdio.lift_io(as_handler=True)
def cape_handler(x):
return statistics.mean(x)
In our first example, the code includes json.loads
and json.dumps
to convert from Python types to bytes. In the second example where we’ve added Serdio, we no longer need this extra step, as Serdio handles it for us automatically.
However, we’ve introduced a new problem. Serdio is not part of the Python standard library, so the Cape enclave won’t know how to import it. We’ll need to add Serdio as a dependency to execute this Cape function in the enclave.
Adding Serdio as a dependency#
We can add the Serdio dependency similarly to how we would add any other third-party dependency. First, we add a requirements.txt
file that specifies the version of Serdio we want to use. In this case, it should be equivalent to the version of PyCape we’re using:
serdio~=1.0.0
Then, we install this into the folder that contains our application code in app.py
. In the previous example, this was the mean
folder which we’ll use again.
pip install serdio --target mean/
Important
For more complicated Python dependencies that include C extension modules, this step will download and/or compile platform- and architecture-specific binaries. Therefore, running the installation step inside a Docker container like python:3.9-slim-bullseye
is heavily recommended by the third-party dependency guide.
Finally, re-deploy this code with cape deploy mean
, and make note of the function ID, checksum, and generate the function token with cape token
.
Running Mean v2 with PyCape#
After re-deploying this code, we can call it from PyCape like we did before.
f = cape.function(function_id, checksum=function_checksum)
t = cape.token(token_file)
x = [1, 2, 3, 4]
result = cape.run(f, t, x, use_serdio=True)
print(f"The mean of x is: {result}")
# Mean of x is: 2.5
Invoking a Cape function multiple times#
If we want to invoke the same Cape function more than once in a Python application, we can take more explicit control PyCape’s connection to the function enclave via a three-step process.
Open the connection with
Cape.connect
Call our function as many times as we’d like via
Cape.invoke
.Close the connection with
Cape.close
f = cape.function(function_id, checksum=function_checksum)
t = cape.token(token_file)
cape.connect(f, t)
low_list = [1, 2, 3, 4]
result = cape.invoke(low_list, use_serdio=True)
print(f"The mean is equal to: {result}")
# The mean is equal to: 2.5
high_list = [5, 6, 7, 8]
result = cape.invoke(high_list, use_serdio=True)
print(f"The mean is equal to: {result}")
# The mean is equal to: 6.5
result = cape.invoke(low_list + high_list, use_serdio=True)
print(f"The mean is equal to: {result}")
# The mean is equal to: 4.5
result = cape.invoke([9, 10, 11, 12], use_serdio=True)
print(f"The mean is equal to: {result}")
# The mean is equal to: 10.5
cape.close()
In fact, the Cape.run
that we used before is simply a convenience function rolling these three commands into one: Cape.run
= Cape.connect
+ Cape.invoke
+ Cape.close
.