Skip to main content

Facts & Fields

A Fact is the typed input schema your rules evaluate. Annotate its attributes with one of the field types and the metaclass machinery wires up storage, defaults, validation, and predicate builders.

Field types

from typing import Literal
from airules import Fact, Field, ListField, NumberField, StringField

Color = Literal["green", "red", "yellow"]

class Light(Fact):
color: Field[Color]
remaining_time: NumberField[int]

class User(Fact):
name: StringField
tags: ListField[str] = ListField(default=None)
TypeExtra predicates
Field[T].eq(...)
NumberField[T].gt · .ge · .lt · .le
StringField.startswith · .endswith · .contains
ListField[E].contains(element)

Fields without an explicit default are required at construction time; passing unknown fields raises TypeError. Optional[...] annotations are inferred as having a default of None.

Embedded facts (dotted paths)

A Fact can hold another Fact and you can build predicates over nested fields with the same syntax you'd use at the top level:

from airules import EmbeddedField, Fact, NumberField, StringField

class Sensor(Fact):
temperature: NumberField[int] = NumberField(default=0)

class Car(Fact):
plate: StringField
sensor: EmbeddedField[Sensor] = EmbeddedField(Sensor, default=None)

Car.sensor.temperature.ge(10) # predicate over the path "sensor.temperature"

If any segment along the path is None, the predicate evaluates to False (the Eq predicate is the exception - it compares None == value honestly, so field.eq(None) works).