Skip to content

Quick Start Guide

Welcome to Predylogic! This guide will help you get up and running with the core features in just a few minutes.

Installation

pip install predylogic

Overview

Predylogic enables you to:

  • Define rules that encapsulate business logic
  • Compose rules using logical operators (AND, OR, NOT)
  • Generate JSON schemas for rule configuration
  • Execute rules dynamically with different contexts
  • Hot-reload rules without restarting your application

1. Registering Rule Definitions

The first step is to create a Registry and register your rule definitions. A registry is a collection of rule definitions for a specific context type.

Basic Setup

from dataclasses import dataclass
from predylogic import Registry


# Define your context type
@dataclass
class User:
    age: int
    email: str
    is_premium: bool


# Create a registry
user_registry = Registry[User]("user_registry")


# Register rule definitions using the decorator
@user_registry.rule_def()
def is_adult(user: User, min_age: int = 18) -> bool:
    """Check if user is at least min_age years old."""
    return user.age >= min_age


@user_registry.rule_def()
def is_premium(user: User) -> bool:
    """Check if user has premium status."""
    return user.is_premium


@user_registry.rule_def()
def is_email_verified(user: User, domain: str = "example.com") -> bool:
    """Check if user email is from a specific domain."""
    return user.email.endswith(f"@{domain}")

Alternative: Direct Registration

If you prefer not to use decorators:

def is_corporate_email(user: User, company_domain: str) -> bool:
    """Check if user has corporate email."""
    return user.email.endswith(f"@{company_domain}")


# Register with explicit name
producer = user_registry.register(is_corporate_email, "has_corporate_email")

Multiple Context Types

You can create separate registries for different context types:

Each context within a registry should be of the same type or satisfy LSP.

from typing import TypedDict


class OrderContext(TypedDict):
    total: float
    item_count: int
    is_bulk: bool


order_registry = Registry[OrderContext]("order_registry")


@order_registry.rule_def()
def high_value_order(order: OrderContext, min_total: float = 1000.0) -> bool:
    return order["total"] >= min_total


@order_registry.rule_def()
def bulk_order(order: OrderContext) -> bool:
    return order["is_bulk"]

2. Exporting JSON Schemas

Once you've defined your rules, you can generate JSON schemas (by pydantic) for external configuration and validation.

Generate Schema

from predylogic import SchemaGenerator

# Create a schema generator
schema_gen = SchemaGenerator(user_registry)

# Generate the manifest pydantic model
UserManifest = schema_gen.generate()

# Export JSON schema
json_schema = UserManifest.model_json_schema()
print(json_schema)

Using the Schema

The generated schema can be used to:

  • Validate rule configurations in external systems
  • Generate API documentation
  • Create UI forms for rule configuration
  • Ensure type safety for JSON inputs
Example
{
  "$defs":{
    "AndNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__":{
      "additionalProperties":false,
      "properties":{
        "node_type":{
          "const":"and",
          "default":"and",
          "description":"And node in the predicate tree",
          "title":"Node Type",
          "type":"string"
        },
        "rules":{
          "description":"All rules must pass",
          "items":{
            "$ref":"#/$defs/LogicNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
          },
          "minItems":2,
          "title":"Rules",
          "type":"array"
        }
      },
      "required":[
        "rules"
      ],
      "title":"AndNode",
      "type":"object"
    },
    "IsAdultConfig":{
      "additionalProperties":false,
      "description":"Check if user is at least min_age years old.",
      "properties":{
        "rule_def_name":{
          "const":"is_adult",
          "default":"is_adult",
          "description":"Name of the rule definition in the registry",
          "title":"Rule Def Name",
          "type":"string"
        },
        "min_age":{
          "default":18,
          "title":"Min Age",
          "type":"integer"
        }
      },
      "title":"IsAdultConfig",
      "type":"object",
      "x-params-order":[
        "min_age"
      ]
    },
    "IsEmailVerifiedConfig":{
      "additionalProperties":false,
      "description":"Check if user email is from a specific domain.",
      "properties":{
        "rule_def_name":{
          "const":"is_email_verified",
          "default":"is_email_verified",
          "description":"Name of the rule definition in the registry",
          "title":"Rule Def Name",
          "type":"string"
        },
        "domain":{
          "default":"example.com",
          "title":"Domain",
          "type":"string"
        }
      },
      "title":"IsEmailVerifiedConfig",
      "type":"object",
      "x-params-order":[
        "domain"
      ]
    },
    "IsPremiumConfig":{
      "additionalProperties":false,
      "description":"Check if user has premium status.",
      "properties":{
        "rule_def_name":{
          "const":"is_premium",
          "default":"is_premium",
          "description":"Name of the rule definition in the registry",
          "title":"Rule Def Name",
          "type":"string"
        }
      },
      "title":"IsPremiumConfig",
      "type":"object",
      "x-params-order":[ ]
    },
    "LeafNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__":{
      "additionalProperties":false,
      "properties":{
        "node_type":{
          "const":"leaf",
          "default":"leaf",
          "description":"Leaf node in the predicate tree",
          "title":"Node Type",
          "type":"string"
        },
        "rule":{
          "anyOf":[
            {
              "$ref":"#/$defs/IsAdultConfig"
            },
            {
              "$ref":"#/$defs/IsPremiumConfig"
            },
            {
              "$ref":"#/$defs/IsEmailVerifiedConfig"
            }
          ],
          "description":"The rule to evaluate",
          "title":"Rule"
        }
      },
      "required":[
        "rule"
      ],
      "title":"LeafNode",
      "type":"object"
    },
    "LogicNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__":{
      "discriminator":{
        "mapping":{
          "and":"#/$defs/AndNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__",
          "leaf":"#/$defs/LeafNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__",
          "not":"#/$defs/NotNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__",
          "or":"#/$defs/OrNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__",
          "ref":"#/$defs/RefNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
        },
        "propertyName":"node_type"
      },
      "oneOf":[
        {
          "$ref":"#/$defs/LeafNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
        },
        {
          "$ref":"#/$defs/AndNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
        },
        {
          "$ref":"#/$defs/OrNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
        },
        {
          "$ref":"#/$defs/NotNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
        },
        {
          "$ref":"#/$defs/RefNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
        }
      ],
      "title":"LogicNode"
    },
    "NotNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__":{
      "additionalProperties":false,
      "properties":{
        "node_type":{
          "const":"not",
          "default":"not",
          "description":"Not node in the predicate tree",
          "title":"Node Type",
          "type":"string"
        },
        "rule":{
          "$ref":"#/$defs/LogicNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__",
          "description":"The rule must fail"
        }
      },
      "required":[
        "rule"
      ],
      "title":"NotNode",
      "type":"object"
    },
    "OrNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__":{
      "additionalProperties":false,
      "properties":{
        "node_type":{
          "const":"or",
          "default":"or",
          "description":"Or node in the predicate tree",
          "title":"Node Type",
          "type":"string"
        },
        "rules":{
          "description":"Any rule must pass",
          "items":{
            "$ref":"#/$defs/LogicNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
          },
          "minItems":2,
          "title":"Rules",
          "type":"array"
        }
      },
      "required":[
        "rules"
      ],
      "title":"OrNode",
      "type":"object"
    },
    "RefNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__":{
      "additionalProperties":false,
      "properties":{
        "node_type":{
          "const":"ref",
          "default":"ref",
          "description":"Reference to a rule definition",
          "title":"Node Type",
          "type":"string"
        },
        "ref_id":{
          "description":"Rule definition ID",
          "title":"Ref Id",
          "type":"string"
        }
      },
      "required":[
        "ref_id"
      ],
      "title":"RefNode[Union[IsAdultConfig, IsPremiumConfig, IsEmailVerifiedConfig]]",
      "type":"object"
    }
  },
  "additionalProperties":false,
  "properties":{
    "registry":{
      "const":"user_registry",
      "default":"user_registry",
      "description":"Name of the registry containing the rule definitions",
      "title":"Registry",
      "type":"string"
    },
    "rules":{
      "additionalProperties":{
        "$ref":"#/$defs/LogicNode_Union_IsAdultConfig__IsPremiumConfig__IsEmailVerifiedConfig__"
      },
      "description":"Dag of rule definitions.",
      "title":"Rules",
      "type":"object"
    }
  },
  "title":"UserRegistryManifest",
  "type":"object"
}

3. Importing Configurations

Create rule configurations from JSON data and validate them against the generated schema.

Simple Configuration

from predylogic import RuleEngine
from predylogic.rule_engine.base import LeafNode

# Create a manifest
manifest = UserManifest(
    rules={
        "is_adult_rule": LeafNode(
            rule={
                "rule_def_name": "is_adult",
                "min_age": 21,
            }
        ),
        "is_premium_rule": LeafNode(
            rule={
                "rule_def_name": "is_premium",
            }
        ),
    }
)

# Manifest is automatically validated against schema
assert manifest.registry == "user_registry"

Complex Configuration (Logical Composition)

from predylogic.rule_engine.base import AndNode, OrNode, NotNode, RefNode

# Create composite rules
manifest = UserManifest(
    rules={
        "premium_adult": AndNode(
            rules=[
                LeafNode(rule={"rule_def_name": "is_adult", "min_age": 18}),
                LeafNode(rule={"rule_def_name": "is_premium"}),
            ]
        ),
        "verified_email": LeafNode(
            rule={
                "rule_def_name": "is_email_verified",
                "domain": "company.com",
            }
        ),
        "special_users": OrNode(
            rules=[
                RefNode(ref_id="premium_adult"),
                RefNode(ref_id="verified_email"),
            ]
        ),
    }
)

Loading from JSON

import json

# Load configuration from file or API
config_data = {
    "rules": {
        "adult_check": {
            "node_type": "leaf",
            "rule": {"rule_def_name": "is_adult", "min_age": 21}
        },
        "premium_check": {
            "node_type": "leaf",
            "rule": {"rule_def_name": "is_premium"}
        },
        "premium_adults": {
            "node_type": "and",
            "rules": [
                {"node_type": "ref", "ref_id": "adult_check"},
                {"node_type": "ref", "ref_id": "premium_check"},
            ]
        }
    }
}

# Validate and create manifest
manifest = UserManifest(**config_data)

4. Retrieving Predicates at Runtime

Use the RuleEngine to execute rules dynamically at runtime.

Basic Execution

from predylogic import RegistryManager
from predylogic.rule_engine import RuleEngine

# Set up manager and engine
registry_manager = RegistryManager()
registry_manager.add_register(user_registry)

engine = RuleEngine(registry_manager)
engine.update_manifests(manifest)

# Get a predicate handle
is_adult_handle = engine.get_predicate_handle("user_registry", "adult_check")

# Execute the predicate
user = User(age=25, email="alice@example.com", is_premium=True)
result = is_adult_handle(user)  # Returns: True

Executing Complex Rules

# Get handle for composite rule
special_users_handle = engine.get_predicate_handle("user_registry", "special_users")

# Execute
result = special_users_handle(user)  # Returns: True/False

Hot Reloading

Update rules at runtime without recreating handles:

# Update manifest with new rules
new_manifest = UserManifest(
    rules={
        "adult_check": LeafNode(
            rule={"rule_def_name": "is_adult", "min_age": 25}
        ),
    }
)

engine.update_manifests(new_manifest)

# Same handle, updated behavior
is_adult_handle = engine.get_predicate_handle("user_registry", "adult_check")
result = is_adult_handle(user)  # Now uses min_age=25

Multiple Registries

order_schema = SchemaGenerator(order_registry).generate()

order_manifest = order_schema(
    rules={
        "high_value": LeafNode(
            rule={"rule_def_name": "high_value_order", "min_total": 5000.0}
        ),
    }
)

registry_manager.add_register(order_registry)
engine.update_manifests(manifest, order_manifest)

# Access rules from different registries
user_handle = engine.get_predicate_handle("user_registry", "adult_check")
order_handle = engine.get_predicate_handle("order_registry", "high_value")

5. Manually Composing Predicates

For dynamic or programmatic composition, create predicates directly without manifests.

Basic Composition

from predylogic import predicate

# Create basic predicates
is_adult = predicate(lambda user: user.age >= 18, name="is_adult")
is_premium = predicate(lambda user: user.is_premium, name="is_premium")

# Compose using operators
premium_adult = is_adult & is_premium
not_premium = ~is_premium
adult_or_premium = is_adult | is_premium

# Execute
user = User(age=25, email="alice@example.com", is_premium=True)
result = premium_adult(user)  # Returns: True

Using ComposablePredicate

from predylogic import ComposablePredicate

# Create predicates from registry
adult_producer = user_registry["is_adult"]
premium_producer = user_registry["is_premium"]

# Produce predicates with parameters
is_adult_21 = adult_producer(min_age=21)
is_premium_check = premium_producer()

# Compose
special_user = is_adult_21 & is_premium_check

# Execute
result = special_user(user)

Logical Operators

# AND - all conditions must be true
condition_and = predicate1 & predicate2 & predicate3

# OR - at least one condition must be true
condition_or = predicate1 | predicate2 | predicate3

# NOT - negate the condition
condition_not = ~predicate1

# Complex expressions
complex_logic = (predicate1 & predicate2) | (~predicate3)

# Execute
result = complex_logic(context)

Trace Execution

Get detailed execution traces for debugging:

# Execute with trace
is_adult = predicate(lambda user: user.age >= 18, name="is_adult")
is_premium = predicate(lambda user: user.is_premium, name="is_premium")
combined = is_adult & is_premium

user = User(age=25, email="alice@example.com", is_premium=True)
trace = combined(user, trace=True)  # Returns Trace object instead of bool

# Inspect the trace
print(f"Operator: {trace.operator}")  # "and"
print(f"Success: {trace.success}")  # True
print(f"Children: {trace.children}")  # [Trace, Trace]

# Print detailed trace
print(repr(trace))

Short-Circuit Evaluation

# Control whether evaluation stops early
combined = predicate1 & predicate2

# With short-circuit (default for bool results)
result = combined(user, short_circuit=True)  # Stops if predicate1 is False

# Full evaluation (useful for traces)
trace = combined(user, trace=True, short_circuit=False)  # Evaluates both

Complete Example

Here's a complete example bringing everything together:

from dataclasses import dataclass
from predylogic import (
    Registry, RegistryManager, SchemaGenerator,
    RuleEngine, predicate
)
from predylogic.rule_engine.base import LeafNode, AndNode


# 1. Define context
@dataclass
class User:
    age: int
    email: str
    is_premium: bool


# 2. Create registry and register rules
user_registry = Registry[User]("user_registry")


@user_registry.rule_def()
def is_adult(user: User, min_age: int = 18) -> bool:
    return user.age >= min_age


@user_registry.rule_def()
def is_premium(user: User) -> bool:
    return user.is_premium


# 3. Generate schema
schema_gen = SchemaGenerator(user_registry)
UserManifest = schema_gen.generate()

# 4. Create configuration
manifest = UserManifest(
    rules={
        "adult": LeafNode(rule={"rule_def_name": "is_adult", "min_age": 21}),
        "premium": LeafNode(rule={"rule_def_name": "is_premium"}),
        "vip_users": AndNode(
            rules=[
                LeafNode(rule={"rule_def_name": "is_adult", "min_age": 21}),
                LeafNode(rule={"rule_def_name": "is_premium"}),
            ]
        ),
    }
)

# 5. Set up engine and execute
registry_manager = RegistryManager()
registry_manager.add_register(user_registry)

engine = RuleEngine(registry_manager)
engine.update_manifests(manifest)

# Get predicates
vip_check = engine.get_predicate_handle("user_registry", "vip_users")

# Execute
user = User(age=25, email="alice@example.com", is_premium=True)
is_vip = vip_check(user)  # True

# Or compose manually
is_adult = predicate(lambda u: u.age >= 21, name="is_adult")
is_premium = predicate(lambda u: u.is_premium, name="is_premium")
vip_manual = is_adult & is_premium

is_vip_manual = vip_manual(user)  # True

Next Steps

  • Read the Modules documentation for detailed API reference
  • Explore Design & Architecture for implementation details
  • Check out the test suite for more usage examples

Common Patterns

Dynamic Rule Building

def create_age_rule(min_age: int):
    return predicate(
        lambda user: user.age >= min_age,
        name=f"min_age_{min_age}"
    )


rule_18 = create_age_rule(18)
rule_21 = create_age_rule(21)

Conditional Composition

def build_user_filter(require_premium=False, min_age=18):
    is_adult = predicate(lambda u: u.age >= min_age, name="is_adult")

    if require_premium:
        is_premium = predicate(lambda u: u.is_premium, name="is_premium")
        return is_adult & is_premium
    else:
        return is_adult


filter_vip = build_user_filter(require_premium=True, min_age=21)
filter_standard = build_user_filter(require_premium=False, min_age=18)

Batch Execution

users = [
    User(age=25, email="alice@example.com", is_premium=True),
    User(age=16, email="bob@example.com", is_premium=False),
    User(age=30, email="charlie@example.com", is_premium=True),
]

vip_check = engine.get_predicate_handle("user_registry", "vip_users")

vip_users = [u for u in users if vip_check(u)]

Happy building with Predylogic! 🚀