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;
pandasandcelare extras. - Two-phase rule contract —
fits(ctx, env)(sync, cheap pre-filter) andasync evaluate(ctx, env)(full evaluation, may do I/O). - Policies —
ALL_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_RUSTtells 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 ⇒
eqshorthand. 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;boolnever coerces to a number;int/floatcoerce 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.
Metadata
Release files for navrules 1.2.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| navrules-1.2.5.tar.gz | 31.7 kB | With provenance | Details for navrules-1.2.5.tar.gz |
Built distributions (wheels)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| navrules-1.2.5-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details for navrules-1.2.5-cp311-abi3-win_amd64.whl |
| navrules-1.2.5-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.17+ x86-64 | Details for navrules-1.2.5-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
Total release size: 1.8 MB
Security for navrules 1.2.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 3 files in this release have been uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.