Dynamically generating SQLAlchemy tables

Background

Here's the background: I had some structured data that I wanted to log to a database. That took the form of Python classes something like this:

class Message:
    code = 0x00
    endianness = '!'
    payload_format = {
        'device_address': 'L',
        'packet_id': 'L',
        'length': 'L',
        'enable': 'L',
    }

    def __init__(self, **kwargs):
        self.fields = kwargs

I wrote a module [struct-parse][struct-parse] sort of for this purpose a few weeks ago. It lets you parse the string format specified in the struct module.

Dynamically declaring classes

The first step was learning how to dynamically define classes in Python. Here's how you declare a class Foo that's a subclass of Bar. It has a class member called device_address that's initialized to None.

Foo = type('Foo', (Bar,) {'device_address': None})

Declaring SQLAlchemy tables

from sqlalchemy import Column, Integer, Float
from sqlalchemy.ext.declarative import declarative_base
import struct_parse as sp


Base = declarative_base()


_type_translation = {
    sp.FieldType.BOOL: Integer,
    sp.FieldType.CHAR: Integer,
    # sp.FieldType.CHAR_ARRAY: Integer,
    sp.FieldType.DOUBLE: Float,
    sp.FieldType.FLOAT: Float,
    sp.FieldType.HALF_PRECISION_FLOAT: Float,
    sp.FieldType.INT: Integer,
    sp.FieldType.LONG: Integer,
    sp.FieldType.LONG_LONG: Integer,
    sp.FieldType.PAD: Integer,
    sp.FieldType.SHORT: Integer,
    sp.FieldType.SIGNED_CHAR: Integer,
    sp.FieldType.SIZE_T: Integer,
    sp.FieldType.SSIZE_T: Integer,
    sp.FieldType.UNSIGNED_CHAR: Integer,
    sp.FieldType.UNSIGNED_INT: Integer,
    sp.FieldType.UNSIGNED_LONG: Integer,
    sp.FieldType.UNSIGNED_LONG_LONG: Integer,
    sp.FieldType.UNSIGNED_SHORT: Integer,
    # sp.FieldType.VOID_POINTER: Integer,
}


class_members = {
    '__tablename__': Message.__name__,
    'id': Column(Integer, primary_key=True),
}

table_classes = {}

for field_name, field_fmt in Message.payload_format.items():
    field_t = sp.parse(Message.endianness + field_fmt)
    class_members[field_name] = Column(_type_translation[field_t])
    TableClass = type('Message', (Base,), class_members)
    table_classes[Message.__name__] = TableClass

This declares a class that models a table to store Message objects. To actually create the database,

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine('sqlite:///log.db')
Base.metadata.create_all(engine)

Now, let's insert a Message object into the Message table. Assume the Message structure definition is in the structures module, and the table is in the schema module.

import structures
import schema

Session = sessionmaker(bind=engine)
session = Session()

msg = structures.Message(
        device_address=0x42,
        packet_id=0x40,
        length=4,
        enable=0)

session.add(schema.table_classes[type(msg).__name__](**msg.fields))

We use type(msg).__name__ to look up the corresponding class that models the table in the schema module. Then, we expand the keyword arguments that msg was initialized with (notice they were stored in structures.Message.__init__) to set the fields of the table.

Done! And this approach seems to work.