Skip to main content

navrules

Generic, low-latency rule evaluation engine with priority-ordered first-match, declarative conditions, and an optional Rust backend for the CPU-bound hot path.

Extracted from a production rewards system; designed to be shared by any consumer that evaluates "subject + environment vs. N rules": rewards/badges, payrate selection at clock-out, eligibility checks, etc.

  • Zero mandatory dependencies — stdlib only; pandas and cel are extras.
  • Two-phase rule contractfits(ctx, env) (sync, cheap pre-filter) and async evaluate(ctx, env) (full evaluation, may do I/O).
  • PoliciesALL_MATCH, ANY_MATCH, FIRST_MATCH (priority-ordered, short-circuiting, typed result payload).
  • Rust hot path — declarative rule sets compile once into a native matcher (PyO3 + rayon, GIL released); pure-Python fallback is always available (navrules.HAS_RUST tells you which one is active).

Quick start: payrate selection at clock-out

from navrules import ConditionRule, EvalContext, Environment, Policy, RuleSet

ruleset: RuleSet[dict] = RuleSet(
    policy=Policy.FIRST_MATCH,
    default={"payrate": 25.0, "code": "BASE"},
)
ruleset.add_rule({
    "rule_type": "ConditionRule",
    "name": "ot_weekend",
    "priority": 100,
    "conditions": {"env.is_weekend": True, "ctx.worked_hours": {"gt": 8}},
    "result": {"payrate": 33.75, "code": "OT-WKND"},
})
ruleset.add_rule({
    "rule_type": "ConditionRule",
    "name": "night_diff",
    "priority": 90,
    "conditions": {
        "env.day_period": {"in": ["evening", "night"]},
        "ctx.job_code": {"in": ["TECH1", "TECH2"]},
    },
    "result": {"payrate": 29.50, "code": "NIGHT"},
})
ruleset.compile()  # sorts by priority, compiles to Rust when available

# hot path — one call per clock-out, no async, no I/O:
env = Environment.at(clockout_ts, tz=site_tz)
ctx = EvalContext.from_user(employee, worked_hours=9.5, job_code="TECH1")
result = ruleset.evaluate_sync(ctx, env)
payrate = result.value["payrate"]

# end-of-day close, thousands of activities (rayon batch):
results = ruleset.evaluate_batch(contexts, env)

Condition DSL

JSON-friendly dict, one entry per field (entries AND together):

{
    "env.is_weekend": true,
    "ctx.worked_hours": {"gte": 4, "lt": 12},
    "ctx.job_code": {"in": ["TECH1", "TECH2"]},
    "quarter": "Q4"
}
  • Scalar value ⇒ eq shorthand. A {op: operand} map applies operators; multiple operators under one field AND together.
  • ctx. prefix reads the evaluation context, env. the environment; bare keys resolve ctx first, then env.
  • Operators: eq ne gt gte lt lte in not_in contains startswith endswith regex between is_null.
  • Missing values match only is_null; bool never coerces to a number; int/float coerce to each other. The Rust backend mirrors these semantics bit-for-bit (enforced by the parity suite).
  • Custom operators can be registered on an OperatorRegistry; rule sets using them transparently fall back to the Python matcher.

Rule families

Family Use case Backend
ConditionRule Declarative conditions, CPU-bound (payrates, ambient rules) Rust or Python
AbstractRule subclass Code rules; evaluate() may hit a DB or service Python (async)
ComputedRule Pull pattern: the rule queries its own candidates Python (async)

Mixed sets work: await ruleset.evaluate(ctx, env) walks rules in priority order, awaiting I/O rules in their turn.

Environment

Environment derives ~35 temporal fields from a single timestamp (is_weekend, quarter, day_period, week_position, is_pay_period, business-day counters...). Environment.at(ts, tz=...) evaluates at any instant and timezone; extra={...} carries site-specific ambient values.

Loading rules from storage

from navrules.storages import FileStorage

async with FileStorage("payrate_rules.json") as storage:
    for spec in await storage.load():
        ruleset.add_rule(spec)
ruleset.compile()

AbstractStorage is the extension point for DB-backed definitions.

Development

# Python-only (fallback backend):
uv run --no-project --with pytest --with pytest-asyncio --with-editable . pytest tests

# Native backend:
make build-rust      # maturin develop --release
cargo test           # crate unit tests (rust/)
RUN_BENCH=1 pytest tests/benchmarks -s

The wheel is built with maturin (abi3-py311: one wheel covers CPython 3.11+). If the native extension cannot be imported, navrules silently runs on the pure-Python matcher — set NAVRULES_DISABLE_RUST=1 to force it.

Download files

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

Source Distribution

navrules-0.1.1.tar.gz (31.6 kB view details)

Uploaded Source

Built Distribution

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

navrules-0.1.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (950.0 kB view details)

Uploaded CPython 3.11+manylinux: glibc 2.17+ x86-64

Release history Release notifications | RSS feed

1.0.1

2 files

1.0.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

1 file

0.1.6

2 files

0.1.5

2 files

0.1.4

2 files

0.1.3

1 file

0.1.2

2 files

This release

0.1.1 This release

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page