Skip to content

Entity schemas

Entity Schema: class

Below is an example of a custom entity schema defined in code. All Liminal entity schema classes inherit from Liminal's BaseModel and uses SQLAlchemy behind the scenes to create an ORM. Liminal provides base classes and clear abstractions to an easy and standardized way to define entity schemas in code. However, you are still able to use raw SQLAlchemy to interact with the schemas when necessary.

The properties defined in the SchemaProperties object and for Column objects align with the properties shown on the Benchling website. This is how Liminal defines your Benchling entity schema in code. Any of these properties can be manipulated to change the definition of the entity schema. Updates to the schema or the addition/archival of schemas are automatically detected by Liminal's migration service, which is run using the liminal autogenerate command. Refer to the First Migration page to run your first migration.

Below, we will go through the different components of defining an entity schema class.

from liminal.orm.relationship import single_relationship
from liminal.orm.schema_properties import SchemaProperties
from liminal.orm.column import Column
from liminal.validation import BenchlingValidator
from liminal.enums import BenchlingEntityType, BenchlingFieldType, BenchlingNamingStrategy
from sqlalchemy.orm import Query, Session
from liminal.orm.mixins import CustomEntityMixin
from liminal.orm.base_model import BaseModel
from pizzahouse.dropdowns import Toppings

class Pizza(BaseModel, CustomEntityMixin):
    __schema_properties__ = SchemaProperties(
        name="Pizza",
        warehouse_name="pizza",
        prefix="PI",
        entity_type=BenchlingEntityType.CUSTOM_ENTITY,
        naming_strategies={
            BenchlingNamingStrategy.REPLACE_NAME_WITH_ID,
            BenchlingNamingStrategy.IDS_FROM_NAMES,
            BenchlingNamingStrategy.NEW_IDS,
        },
        mixture_schema_config=None,
    )

    dough = Column(name="dough", type=BenchlingFieldType.ENTITY_LINK, required=True, entity_link="dough")
    cook_temp = Column(name="cook_temp", type=BenchlingFieldType.INTEGER, required=False)
    cook_time = Column(name="cook_time", type=BenchlingFieldType.INTEGER, required=False)
    toppings = Column(name="toppings", type=BenchlingFieldType.DROPDOWN, required=False, dropdown=Toppings)
    customer_review = Column(name="customer_review", type=BenchlingFieldType.INTEGER, required=False)
    slices = Column(name="slices", type=BenchlingFieldType.ENTITY_LINK, required=False, is_multi=True, entity_link="slice")

    dough_entity = single_relationship("Dough", dough)
    slice_entities = multi_relationship("Slice", "Pizza", "slices")

    def __init__(
        self,
        dough: str,
        cook_temp: int | None = None,
        cook_time: int | None = None,
        customer_review: int | None = None,
    ):
        self.dough = dough
        self.cook_temp = cook_temp
        self.cook_time = cook_time
        self.customer_review = customer_review


    @classmethod
    def query(self, session: Session) -> Query:
        return session.query(Pizza)

    def get_validators(self) -> list[BenchlingValidator]:
        return []

Mixins: class

All Liminal entity schema classes must inherit from one of the mixins in the mixins module. The mixin provides the base columns for the specific entity schema type. For example, the CustomEntityMixin provides the base columns for a custom entity schema. To learn more, check out the SQLAlchemy documentation here.

Schema Properties: class

Parameters

name: str

The name of the entity schema. Must be unique across all entity schemas. warehouse_name: str

The warehouse name of the entity schema. Must be unique across all entity schemas.

Note

The warehouse names are used as keys across liminal and are used as entity_link values in Columns.

prefix: str

The prefix of the entity schema. Must be unique across all entity schemas.

entity_type: BenchlingEntityType

The type of entity schema. Type must be one of the values from the BenchlingEntityType enum.

naming_strategies: set[BenchlingNamingStrategy]

The naming strategies for the entity schema. Must be a set of values from the BenchlingNamingStrategy enum.

mixture_schema_config: MixtureSchemaConfig | None

The mixture schema configuration for the entity schema. Must be defined as a MixtureSchemaConfig object.

Column: class

Parameters

name: str

The external facing name of the column.

type: BenchlingFieldType

The type of the field. Type must be one of the values from the BenchlingFieldType enum.

required: bool

Whether the field is required.

is_multi: bool = False

Whether the field is a multi-value field. Defaults to False.

parent_link: bool = False

Whether the field is a parent link field. Defaults to False.

dropdown: Type[BaseDropdown] | None = None

The dropdown object for the field. The dropdown object must inherit from BaseDropdown and the type of the Column must be BenchlingFieldType.DROPDOWN. Defaults to None.

entity_link: str | None = None

The entity link for the field. The entity link must be the warehouse_name as a string of the entity schema that the field is linking to. The type of the Column must be BenchlingFieldType.ENTITY_LINK in order to be valid. Defaults to None.

tooltip: str | None = None

The tooltip for the field. Defaults to None.

Relationships: module

If there are columns that are entity links, that means the value of the column is the linked entity id or ids. You can easily define relationships using Liminal's wrapper functions around SQLAlchemy. The two relationships to define are single_relationship and multi_relationship, and examples are shown above.

# single_relationship is used for a non-multi field where there is a one-to-one relationship from the current class to the target class.
single_relationship(target_class_name: str, entity_link_field: Column, backref: str | None = None) -> RelationshipProperty:

# multi_relationship is used for a multi field where there is a "one-to-many" relationship from the current class to the target class.
# NOTE: This is not a normal one-to-many relationship. The multi field is represented as a list of entity ids.
multi_relationship(target_class_name: str, current_class_name: str, entity_link_field_name: str) -> RelationshipProperty

How do I access the joined entity or entities?

connection = BenchlingConnection(...)
benchling_service = BenchlingService(connection, use_db=True)

with benchling_service as session:
    pizza_entity = session.query(Pizza).first()

    # NOTE: Accessing the relationship entities must be done within the session context.
    dough = pizza_entity.dough_entity
    slices = pizza_entity.slice_entities

Custom Query

The query() method must be implemented for the entity schema class to define a custom query. This is useful if you want to add additional filtering or joins to the query.

Validators: class

As seen in the example above, the get_validators method is used to define a list of validators for the entity schema. These validators run on entities of the schema that are queried from Benchling's Postgres database. For example:

pizza_entity = Pizza.query(session).first()

# Validate a single entity from a query
report = CookTempValidator().validate(pizza_entity)

# Validate all entities for a schema
reports = Pizza.validate(session)

The list of validators within get_validators are used to run on all entities of the schema.

The BenchlingValidator object is used to define the validator classes, that can be defined with custom logic to validate entities of a schema. Refer to the Validators page to learn more about how to define validators.

Additional Functionality

Below is additional functionality that is provided by the Liminal BaseModel class.

connection = BenchlingConnection(...)
benchling_service = BenchlingService(connection, use_db=True)

with benchling_service as session:

    # Get all entities for a schema and return a dataframe
    df = Pizza.df(session)

    # Validate all entities for a schema and return a list of ValidatorReports
    reports = Pizza.validate(session)

    # Validate all entities for a schema and return a dataframe
    validated_df = Pizza.validate_to_df(session)

Notes

  • Note that the Entity Schema definition in Liminal does not cover 100% of the properties that can be set through the Benchling website. However, the goal is to have 100% parity! If you find any missing properties that are not covered in the definition or migration service, please open an issue on Github. In the meantime, you can manually set the properties through the Benchling website.