Building a Database REST API with Python
By: Joshua Jamison
Tags: API, Database, Rest Api, Asyncio, Python

Database Layer

server = FastAPI()

The FastAPI() instance server reference is our primary URL router for Client Requests in the API Layer, and a Websocket Router for EasyRpcServer in the Database Layer.

@server.on_event('startup')

FastAPI on_event provides two useful hooks for performing setup / tear-down when the application starts. on_event(startup|shutdown)

# Rpc Server

rpc_server = await EasyRpcServer.create(

server,

'/ws/database',

server_secret='abcd1234'

)

EasyRpcServer.create is a class factory method which returns a EasyRpcServer() instance: using our FastAPI server, web-socket path(where the ASGI server will accept Websocket negotiation), server_secret( the password that a EasyRpcProxy will use to connect), and optionally encryption_enabled=True|False.

The EasyRpcServer reference, rpc_server, will be used to register our database functions in a namespace, which will be shared with connecting EasyRpcProxies — more on that in a bit.

db = await Database.create(

database='easy_db',

cache_enabled=True

)

Database.create is a factory method which returns a Database() instance, using database=name, cache_enabled=True|False. The default database-type is sqlite, so minimal arguments are required. See aiopyql for more details.

aiopyql is used here for its simplicity, support for asyncio, and caching features which lends itself nicely when justifying the API / Database Layer separation. The patterns used can be translated into any ORM for sharing database access.

# Table Creation

if not 'keystore' in db.tables:

await db.create_table(

'keystore',

[

('key', str, 'UNIQUE NOT NULL'),

('value', str)

],

prim_key='key',

cache_enabled=True

)

Our Database() instance ‘db’, automatically discovers table schemas (if any exist) and creates a Table() object within db.tables.

With the above check, a table named ‘keystore’ is created with a very simple key-value schama. This can be thought of persistent python dictionary — with the added bonus of caching.

server.data = {'keystore': db}

The Database instance reference is stored in our FastAPI Router — needed for cleanup steps.

# register each table method in table namespace

for table in db.tables:

for func in {'select', 'update', 'insert', 'delete'}:

rpc_server.origin(

getattr(db.tables[table], func), namespace=table)

server.db_server = db_server

Each Table() instance within db.tables contain select, update, insert, and delete methods.

For every function, the EasyRpcServer() instance .origin method is called, using the function reference, and namespace=table_name.

If there is more than one table, the above example would register each of these methods in a namespace matching the table name.

What does register actually do?

The register method “dictionarifies” the functions meta-data, and stores this in a namespace. By meta-data, I mean everything used to call the method(arguments, argument types, method name).

EasyRpcProxy instances use this function meta-data to “mirror” a proxy function, and verify correct input.



async def shutdown():

await server.data['keystore'].close() @server .on_event('shutdown')async def shutdown():await server.data['keystore'].close()

The housekeeping shutdown method that allows our instance to gracefully cleanup.