January 10, 2024

Leveraging Python Standard Library via WebAssembly

Tim McCallum Tim McCallum

spin python math

Leveraging Python Standard Library via WebAssembly

This article delves into using Python for solving mathematical problems over the web. We provide hands-on Python examples, compiled to WebAssembly (Wasm) and showcase how easy it is to use Python libraries to create Wasm-powered serverless applications. We also provide instructions on deploying to Fermyon Cloud and make the examples in this article accessible, for you, via the web.

A recent article of ours tackles the significant issue of code duplication. The article titled “It’s Time to Reboot Software Development” explains inefficiencies in traditional software development, particularly highlighting the redundancy in creating multiple versions of the same logic across different programming languages. The URL parser example given in the aforementioned blog post is spot on. Duplicating uncomplicated logic over and over is what we used to do because we had to. Thankfully, the Wasm Component Model is well and truly paving the way for cross-language composition inside a single application. This potentially means a reduction in redundant code and the ability to choose which programming language to use for a given component of our application.

Would You Rather?

Would you rather program precision mathematics using Javascript or Python?

JavaScript is often referred to as the primary programming language of the web, used to create interactive and dynamic content on websites. And as time has proven, Javascript is not going anywhere, which is great!

Python, with its extensive libraries and tools for scientific computation, tends to be more reliable for high-precision mathematical operations. JavaScript, while capable of basic arithmetic, may not be as suitable for tasks requiring the highest levels of numerical accuracy.

For this reason, let’s use the experimental Spin Python SDK and leverage some of the Python Standard Library to solve some mathematical problems over the web.

We have many concrete examples coming up but at this early stage, it is worth mentioning that there are some caveats e.g. Python’s implementation of TCP and UDP sockets, as well as Python libraries that use threads, processes, and signal handling behind the scenes, will not compile to Wasm at present. The good news is that whilst a decent share of the Python Standard Library can be compiled to Wasm, libraries found on pypi.org (the pure Python libraries; not C/C++ or Fortran code) should also work (similarly considering the same above caveats).

Let’s try out a couple of libraries now.

Example: Finding Greatest Common Divisor

The following example shows how easy it is to find the Greatest Common Divisor (GCD) given a list of numbers:

import math
import json
from functools import reduce
from spin_http import Response


def handle_request(request):
    if request.method == 'POST':
        numbers = json.loads(request.body.decode("utf-8"))
        answer = {"Greatest Common Divisor (GCD)": reduce(math.gcd, numbers)}
        return Response(200,
                        {"content-type": "text/plain"},
                        bytes(json.dumps(answer),"utf-8"))

As you can see from the above example, we are writing one-liners like reduce(math.gcd, [12, 18]) to solve GCD. This is super quick, easy and reliable – what libraries were intended for.

You may have noticed that we are building this Python code via the Spin framework. When using Spin and Fermyon Cloud, we are making this functionality available via the web. For example, once the above function is deployed to Fermyon Cloud, we can call this function using the following format (and obtain the appropriate response):

$ curl -X POST "https://my.fermyon.app" -H "Content-Type: application/json" -d '[12, 18]'
{"Greatest Common Divisor (GCD)": 6}

Let’s Create a Python/Wasm Application Together

To follow along please go ahead and install or upgrade Spin on your system (make sure you have at least Spin v2).

YouTube Video

Let’s scaffold a new application using the http-empty template, in line with the recommended application structure from this blog article:

$ spin new -t http-empty
Enter a name for your new application: python-math-functions
Description: An application that makes Python math libraries available as Wasm-powered serverless functions 

From here we add a new component that can calculate the greatest common divisor:

$ spin add -t http-py gcd
Description: Calculates Greatest Common Divisor (GCD)
HTTP path: /gcd/...

After performing the above steps our application structure is as follows:

$ tree .
.
├── gcd
│   ├── app.py
│   ├── Pipfile
│   └── README.md
└── spin.toml

We can now go ahead and populate our gcd/app.py file with the same source code we used above e.g.:

import math
import json
from functools import reduce
from spin_http import Response


def handle_request(request):
    if request.method == 'POST':
        numbers = json.loads(request.body.decode("utf-8"))
        answer = {"Greatest Common Divisor (GCD)": reduce(math.gcd, numbers)}
        return Response(200,
                        {"content-type": "text/plain"},
                        bytes(json.dumps(answer),"utf-8"))

Let’s add yet another component, this time a fairly complex function that simulates arrival times and service deliveries for a multiserver queue:

$ spin add -t http-py simulation-multiserver-queue
Description: Simulation of arrival times and service deliveries for a multiserver queue:
HTTP path: /simulation-multiserver-queue/...

After performing the above steps our application structure is as follows:

$ tree .
.
├── gcd
│   ├── app.py
│   ├── Pipfile
│   └── README.md
├── simulation-multiserver-queue
│   ├── app.py
│   ├── Pipfile
│   └── README.md
└── spin.toml

We can now go ahead and populate our simulation-multiserver-queue/app.py file with the following source code:

import json
from spin_http import Response
from random import expovariate, gauss
from heapq import heapify, heapreplace
from statistics import mean, quantiles

def handle_request(request):
    if request.method == 'POST':
        json_str = request.body.decode('utf-8')
        json_object = json.loads(json_str)
        average_arrival_interval = json_object["average_arrival_interval"] # e.g. 5.6
        average_service_time = json_object["average_service_time"] # e.g. 15.0
        stdev_service_time = json_object["stdev_service_time"] # e.g. 3.5
        num_servers = json_object["num_servers"] # e.g. 3
        waits = []
        arrival_time = 0.0
        servers = [0.0] * num_servers  # time when each server becomes available
        heapify(servers)
        for i in range(1_000_000):
            arrival_time += expovariate(1.0 / average_arrival_interval)
            next_server_available = servers[0]
            wait = max(0.0, next_server_available - arrival_time)
            waits.append(wait)
            service_duration = max(0.0, gauss(average_service_time, stdev_service_time))
            service_completed = arrival_time + wait + service_duration
            heapreplace(servers, service_completed)
        mean_wait = mean(waits)
        max_wait = max(waits)
        quartiles = [round(q, 1) for q in quantiles(waits)]
        answer = {"Mean wait": mean_wait, "Max wait": max_wait, "Quartiles": quartiles}
    return Response(200,
                    {"content-type": "text/plain"},
                    bytes(json.dumps(answer),"utf-8"))

We can now build our application:

$ spin build
Building component gcd with `spin py2wasm app -o app.wasm`
Working directory: "./gcd"
Spin-compatible module built successfully
Building component simulation-multiserver-queue with `spin py2wasm app -o app.wasm`
Working directory: "./simulation-multiserver-queue"
Spin-compatible module built successfully
Finished building all Spin components

With our application built, we can deploy to Fermyon Cloud and send requests to each of the separate components:

$ spin deploy
Uploading python-math-functions version 0.1.0 to Fermyon Cloud...
Deploying...
Waiting for application to become ready...
Available Routes:
  gcd: https://python-math-functions-bfq1yvcc.fermyon.app/gcd (wildcard)
  simulation-multiserver-queue: https://python-math-functions-bfq1yvcc.fermyon.app/simulation-multiserver-queue (wildcard)

Let’s first go ahead and call the GCD function:

$ curl -X POST "https://python-math-functions-bfq1yvcc.fermyon.app/gcd" -H "Content-Type: application/json" -d '[12, 18]'

For which we receive the following correct greatest common divisor:

{
    "Greatest Common Divisor (GCD)": 6
}

Lastly, we can call the simulation (passing in an average_arrival_interval of 5.6, an average_service_time of 15.0, a stdev_service_time of 3.5 and the num_servers value of 3):

$ curl -X POST "https://python-math-functions-bfq1yvcc.fermyon.app/simulation-multiserver-queue" -H "Content-Type: application/json" -d '{"average_arrival_interval": 5.6, "average_service_time": 15.0, "stdev_service_time": 3.5, "num_servers": 3}'

The above request returns the following JSON object:

{
    "Mean wait": 20.22066529829912,
    "Max wait": 240.50462200650054,
    "Quartiles": [
        2.2,
        12.7,
        29.4
    ]
}

Leveraging these Python libraries makes short work of very complex math problems. Python’s motto of “batteries included” certainly rings true here. What other ideas do you have for writing and deploying Wasm-powered serverless applications for the web? Visit our official developer documentation and jump on Discord if you want to meet some of our team, discuss your application ideas or get pointers on how to progress your Wasm-powered applications.

Thanks for reading!


🔥 Recommended Posts


Quickstart Your Serveless Apps with Spin

Get Started