Skip to main content

An embedded, composable schema-driven predicate logic engine.

Project description

PredyLogic

Ruff ty CodSpeed codecov PyPI - Python Version

PyPI - Status GitHub Actions Workflow Status Docs Commit activity License

An embedded, composable schema-driven predicate logic engine.

Inspiration: Heavily inspired by the architectural concepts discussed by ArjanCodes.

We are still in v0.x, breaking changes may occur between minor versions.

About Name

predy (adj.) Archaic British. Nautical.

  1. (of a ship) prepared or ready for sailing or action.
  2. to make the ship ready for battle (e.g., "predy the decks").
    Collins English Dictionary

predylogic takes its name from this concept. It represents logic that is not hardcoded into the flow of battle, but defined, cleared for action, and "predy" for execution.

It also serves as a nod to Predicate Logic.

Overview

predylogic is a headless, composable predicate logic engine for Python.

It decouples business logic from control flow by treating rules as data, not code blocks. Unlike heavy-weight rule engines (e.g., Drools OAP ) or simple if/else spaghetti, predylogic sits in the middle: it offers strong type safety, * zero external dependencies*, and deferred execution.

It represents the shift from imperative control flow (hardcoded if/else checks) to declarative predicate definitions. The goal is to make logic "ready" (predy) for serialization, composition, and reuse.

Designed for developers who need to define rules in Python, serialize them (planned), and execute them against strict data contexts.

Why PredyLogic?

Applications often struggle with "Logic Sprawl":

  1. **Hardcoded if/else**: Fast but rigid. Changing logic requires code deployment.
  2. Configuration Spaghetti: JSON/YAML files that are untyped, hard to validate, and impossible to debug. (Then it becomes a demonstration of Greenspun's tenth rule.)
  3. Heavy Rule Engines: Overkill solutions (Java-based or OPA like) that introduce significant latency and infrastructure complexity.

PredyLogic bridges the gap. It adopts a Hybrid Architecture:

  • Define in Code: Atomic logic (the "What") is written as pure, testable Python functions.
  • Compose in Data: Logic flow (the "How") is structured as data, loaded dynamically, and compiled at runtime.

It provides the flexibility of a rule engine with the performance of native code.

Key Features

  • Zero Infrastructure Dependencies: No JVM, no sidecars, no external API servers. It runs entirely in-process using the modern Python stack.
  • Native-Level Performance: Rules are not interpreted step-by-step; they are compiled into flat Python bytecode. The abstraction cost is near-zero (comparable to handwritten code).
  • Atomic Rule Factories: Define your basic building blocks (is_vip, amount_gt) as plain Python functions. Compose them dynamically from configuration files without changing code.
  • Schema-Driven Validation: Export a dynamic JSON Schema from your registry to validate your rule configurations. Catch logic errors (e.g., passing a string to an integer field, using a non-existent rule definition.) at config time, not runtime.
  • Audit-Ready Execution: Logic is no longer a black box. Trace every decision path to understand exactly why a rule matched or failed (e.g., for compliance or debugging).

Quick Start

Install the package:

pip install predylogic

View the online documentation

1. Define what can be checked. These are your stable building blocks.

from typing import TypedDict
from predylogic import Registry, all_of, any_of


# 1. Define the Context (Protocol or dataclass or Pydantic BaseModel, or any other type)
class Transaction(TypedDict):
    amount: int
    region: str
    is_fraud_flagged: bool


# 2. Initialize Registry
registry = Registry[Transaction]("transaction_rules")


# 3. Define Atomic Predicates
@registry.rule_def()
def is_high_value(ctx: Transaction, threshold: int = 1000) -> bool:
    return ctx["amount"] >= threshold


# Define aliases using parameter
@registry.rule_def("check_region")
def in_regions(ctx: Transaction, regions: list[str]) -> bool:
    return ctx["region"] in regions


@registry.rule_def()
def is_safe(ctx: Transaction) -> bool:
    return not ctx["is_fraud_flagged"]

The @rule_def decorator transforms your function into a curried closure factory. In type hint terms, it shifts the signature from Callable[Concatenate[T, **P], bool] to Callable[**P, Callable[[T], bool]].

For example: You define this:

def is_high_value(ctx: Transaction, threshold: int = 1000) -> bool:
    return ctx["amount"] >= threshold

The decorator transforms it conceptually into this:

def is_high_value(threshold: int = 1000) -> Callable[[Transaction], bool]:
    return lambda: ctx: ctx["amount"] >= threshold

This allows you to "pre-configure" the rule with arguments (partially apply it): is_costly = is_high_value(2000)

2. Dynamic Composition (Simulation)

In a real app, this structure would be loaded from a JSON/YAML file or from database. Here we construct it to show the API.

# Rule: "Safe AND (High Value OR In Target Region)"
# This structure validates against the rule_engine derived from the registry.


# Alternatively, you may utilise the __and__, __or__, and __invert__ overloads (| & ~).
#   For extensive isomorphic combinations, use `all_of/any_of` to improve performance.

policy = all_of(
    [
        is_safe(),
        any_of(
            [
                is_high_value(2000),
                in_regions(["US", "EU"]),
            ]
        ),
    ]
)

# The 'policy' object is now compiled and ready for hot-loop execution.

Internally, the engine uses lazy bytecode compilation to flatten this composition into raw bytecode, so the execution speed matches handwritten and/or chains with close zero abstraction cost. (Detailed profiling is available in the ADRs).

3. Execution & Trace

tx_data = {"amount": 500, "region": "US", "is_fraud_flagged": True}

# Execute (Fast Path)
assert policy(tx_data) is False

# Execute with Audit Log (Slow Path)
trace = policy(tx_data, trace=True, short_circuit=False)
assert not policy(tx_data)
print(trace)
# >>> Output:
# ❌ AND
#  ❌ is_safe
#    └─ Context: {'amount': 500, 'region': 'US', 'is_fraud_flagged': True}
#  ✅ OR
#    ❌ is_high_value
#      └─ Context: {'amount': 500, 'region': 'US', 'is_fraud_flagged': True}
#    ✅ in_regions

NOTE: The trace functionality is currently undergoing iteration. Additional information will be incorporated. You can customize the TraceStyle to fit your logging system.

4. Serde:

PredyLogic supports the combination of rules through configuration orchestration.

Export JSON schema

from predylogic import SchemaGenerator

# here is the standard pydantic BaseModel.
Manifest = SchemaGenerator(registry).generate()
print(Manifest.model_json_schema())

Import from configuration

from predylogic import RegistryManager, RuleEngine

manager = RegistryManager()
manager.add_register(registry)
json_data = """
    {
  "registry":"transaction_rules",
  "rules":{
    "policy":{
      "node_type":"and",
      "rules":[
        {
          "node_type":"leaf",
          "rule":{
            "rule_def_name":"is_safe",
            "params":{}
          }
        },
        {
          "node_type":"or",
          "rules":[
            {
              "node_type":"leaf",
              "rule":{
                "rule_def_name":"is_high_value",
                "params":{
                  "threshold":2000
                }
              }
            },
            {
              "node_type":"leaf",
              "rule":{
                "rule_def_name":"check_region",
                "params":{
                  "regions":[
                    "US",
                    "EU"
                  ]
                }
              }
            }
          ]
        }
      ]
    }
  }
}
"""

manifest = Manifest.model_validate_json(json_data)
engine = RuleEngine(manager)
engine.update_manifests(manifest)

policy = engine.get_predicate_handle("transaction_rules", "policy")
assert policy(tx_data) is False

PredyLogic permits runtime updates to predicates. For further details, please consult the online documentation.

Under the Hood: The Engineering

PredyLogic is not just a collection of helper functions; it is a domain-specific language (DSL) compiler built on strict computer science principles.

1. Algebraic Structures (Monoids)

Boolean operators (AND, OR) form a Monoid. They are associative ((A & B) & C == A & (B & C)) and have an identity element (True for AND, False for OR). We leverage this mathematical property to perform AST Flattening. A deeply nested tree of binary operations (depth 2000+) is algebraically reduced to a single N-ary operation during the compilation phase, enabling stack usage at runtime.

2. Lazy Bytecode Compilation

Instead of interpreting the rule tree recursively (which is slow and stack-limited), PredyLogic acts as an embedded compiler.

  • AOT Construction: Rule definitions are validated and constructed as data structures.
  • Lazy Bytecode: Compilation: Upon the first execution (or explicit compilation), the object tree is transformed into Python's native ast (Abstract Syntax Tree) and compiled into into native Python bytecode on first execution and cached. This means your logic runs at the speed of native Python opcodes (JUMP_IF_FALSE, etc.), bypassing the overhead of function calls and object dispatch.

3. Type Theory (Contravariance)

The Predicate[T] type is contravariant in T.

This ensures strict type safety in a polymorphic context: A rule expecting a generic Transaction context can safely handle a more specific FraudTransaction context, but not vice versa.

This prevents runtime AttributeError by catching schema mismatches during static analysis (MyPy/Pyright/ty).

4. Partial Application (Currying)

The engine strictly separates Logic from Configuration via Partial Application.

When you invoke a rule factory like is_high_value(threshold=1000), you are binding the parameters (Configuration) to the function (Logic) before execution. This transforms a generic multi-argument function into a specialized single-argument predicate (CTX -> bool).

This makes testing trivial: since your atomic rule definitions are typically pure functions, you can verify complex business logic with simple unit tests—no mocks, no fixtures, and no database required.

Roadmap

Next up: a minimal DSL to replace raw JSON/YAML configuration, followed by a CLI with schema-driven validation and LSP support.

For the configuration above json, using DSL looks like

## transaction_rules

policy = is_safe() & (is_high_value(2000) | check_region(regions=["US", "EU"]))

Or

## transaction_rules

expensive = is_high_value(2000)
policy = is_safe() & (expensive | check_region(regions=["US", "EU"]))

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

predylogic-0.1.0.tar.gz (47.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

predylogic-0.1.0-py3-none-any.whl (28.6 kB view details)

Uploaded Python 3

File details

Details for the file predylogic-0.1.0.tar.gz.

File metadata

  • Download URL: predylogic-0.1.0.tar.gz
  • Upload date:
  • Size: 47.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for predylogic-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ee1812120e11ee85449a1d7a41edfde56b493e9920b17dfa2e0740ec17887c25
MD5 724d150566a9a2f37ba7de2d65605429
BLAKE2b-256 ef99ae486a7047150209b309b8b5fd7a9877244c4f10cdc584687b23e31206f2

See more details on using hashes here.

File details

Details for the file predylogic-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: predylogic-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 28.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for predylogic-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 efff104fa47b2a2ffa8aeb8834fc71744e3c3cb5bedc19bc207092dc371f1388
MD5 4cfb6a7d222c7cac4fdf2d2a1b6404ab
BLAKE2b-256 070f110092124fdc4d8aa367d0b407aeca9a9993a27dc686900889d1f5a59d9e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page