Introduction
In seeking a lightweight framework to implement a REST API as an application back-end, Flask quickly rose to the top of my list. Flask is a strong choice for creating an API, and it also fit my secondary goal of letting me flex my Python skills, a language that up until now I had only used for some data analysis projects.
Flask is “a lightweight WSGI web application framework. It is designed to make getting started quick and easy, with the ability to scale up to complex applications. [Flask] has become one of the most popular Python web application frameworks.” This was speaking my language: simple, lightweight, standards-based, and the ability to scale up. Simplicity alone was very important to me, as I began to bulk up my skill and confidence in Python.
In addition to using Flask, I wanted to embrace the “API first” design principle and ideally adopt the OpenAPI specification. I found a great tutorial for doing just this in Flask, and using Connexion which is built just for this purpose!
With the frameworks selected and a great tutorial as a guide, the only remaining thing needed was a proof-of-concept as a first creation. Let’s have a bit of fun. Let’s build Commodore 64 as a Service.
OpenAPI and Connexion
OpenAPI allows you to define your REST API in a YAML or JSON file. The definition includes at minimum the supported API endpoints and operations, operation parameters, response output, and any authentication. Below is an excerpt of the OpenAPI definition for C64aaS.
paths:
/program:
get:
operationId: "program.read_all"
summary: "LIST the entire program."
responses:
"200":
description: "Successfully read program LISTing."
post:
operationId: "program.create"
summary: "Create a NEW program."
requestBody:
description: "Program to create"
required: True
content:
application/json:
schema:
x-body-name: "program"
$ref: "#/components/schemas/ProgramList"
responses:
"201":
description: "Successfully created program."
Connexion will read the OpenAPI specification (in swagger.yml) and handle the HTTP requests, routing to the correct Python function to handle them, and sending the response.
import connexion
app = connexion.App(__name__, specification_dir="./")
app.add_api("swagger.yml")
@app.route("/")
...
app.run(host="0.0.0.0", port=6464, debug=True)
Connexion uses the operationId
parameter for each path to determine which Python function should handle the request. For example, on a POST operation to the /program URL, the create
method will be called within the program.py
module. The allows you to be modular with the code supporting each endpoint (resource) for you API. Connexion also support a few methods of automatic routing if you do not want to explicitly define each supporting function.
With OpenAPI, you can also define and enforce requirements on the structure of the request. For a POST to /program, a request of content-type application/json
is required and the contents must follow the ProgramList object schema.
paths:
/program:
...
post:
operationId: "program.create"
summary: "Create a NEW program."
requestBody:
description: "Program to create"
required: True
content:
application/json:
schema:
x-body-name: "program"
$ref: "#/components/schemas/ProgramList"
The ProgramList schema is also defined in the specification as an array of Program objects according to that corresponding schema as shown below.
The Program schema defines it be an object with an integer property called line and a string property called input. Both line and input are required for the Program object. (When creating a program on the Commodore 64, each program line must begin with an integer line number followed by a string of BASIC input for that line.)
components:
schemas:
Program:
type: "object"
required:
- line
- input
properties:
line:
type: "integer"
input:
type: "string"
example:
line: 10
input: "PRINT \"HELLO WORLD\""
ProgramList:
type: array
items:
oneOf:
- $ref: "#/components/schemas/Program"
So when a request is handled by the create method in the program.py module, we can know exactly what to expect when it comes to the request contents.
# program.py
def create(program):
PROGRAM = []
for l in program:
line = l.get("line")
input = l.get("input")
if input:
element = {
"input": input,
"line": line,
}
PROGRAM.append(element)
else:
abort(
406,
"Invalid program request",
)
with open(filename, "w") as file:
file.write(json.dumps(PROGRAM)) # write PROGRAM to file
return PROGRAM, 201
Swagger UI Documentation
Navigating to the /app/ui URL will show the very nice Swagger UI interactive REST documentation for C64aaS shown below. All of this is automatically generated by Connexion!
C64aaS In Action
If you spin open the POST endpoint for Program in Swagger UI, you get an interactive form allowing you to specify the content-type and body of the Request. You can even put an example Request in your swagger.yml file, which will appear here to give the user a head start.
Clicking the Execute button will send the request to your API application and receive the response, displaying the headers and body of each, as can be seen below.
You can also interact with the GET methods, supplying any accepted parameters to see the results returned by the API. Sending a GET to the Program endpoint returns the contents of the program that was just created when you submitted the POST above.
Sending a GET to the Run endpoint will instruct the API to run the Program you created with the Commodore BASIC interpreter running on the back-end server. The results of that program run are then sent back as a JSON response.
As you can see, the BASIC program we created with a POST to Program:
10 REM SAY HELLO
20 PRINT "HELLO ADAM"
Results in the following response from the Run endpoint of our API:
[ "HELLO ADAM" ]
For anyone who has programmed BASIC on 8-bit computers, you will know that infinite loops are common in example programs such as the famous 10 PRINT classic:
10 PRINT CHR$(205.5+RND(1)); : GOTO 10
The current version of Commodore 64 as a Service roughly supports support PETSCII graphics via a Unicode translation (see also this thread), so I wanted to make sure the API could handle a infinite loops. It does this by implementing a timeout on the execution of Run, returning the results produced before the program was stopped due to timeout.
Here is another even simpler program with an infinite loop:
10 PRINT "HELLO WORLD"
20 GOTO 10
Submitting this infinite loop to the GET Run API for Commodore 64 as a Service returns several thousand HELLO WORLD! lines as shown in the video below.
Commodore 64 as a Service was a fun way to learn Flask for Python using Connexion to support an “API first” implementation. It’s certainly a bit silly, but perhaps in a world filled with blockchain currencies, the metaverse, and ChatGPT large language models, then having Commodore 64 as a Service available for all will make the internet is a better place!
Sources and Credits:
The title image at the top of the post is “Cyberpunk Lamers” by Odyn1ec (Jarek Wyszyñski).
The tutorial mentioned is Python REST APIs With Flask and Connexion written by Philipp Acsany.
My Commodore 64 as a Service repository is available on GitHub.