Metadata-Version: 2.5
Name: fluxrules
Version: 0.1.4
Summary: A light-weight Business Rule Engine
Project-URL: Homepage, https://saurabh2mishra.github.io/fluxrules/
Project-URL: Repository, https://github.com/saurabh2mishra/fluxrules
Project-URL: Documentation, https://saurabh2mishra.github.io/fluxrules/
Project-URL: Changelog, https://github.com/saurabh2mishra/fluxrules/blob/main/CHANGELOG.md
Project-URL: Bug Tracker, https://github.com/saurabh2mishra/fluxrules/issues
Author: FluxRules Contributors
Author-email: Saurabh Mishra <saurabh2.mishra@gmail.com>
License: MIT
License-File: LICENSE
Keywords: business-rules,inference,phreak,rule-evaluation,rules-engine
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pydantic>=2.8
Provides-Extra: all
Requires-Dist: alembic>=1.13; extra == 'all'
Requires-Dist: bcrypt>=4.0; extra == 'all'
Requires-Dist: click>=8.0; extra == 'all'
Requires-Dist: email-validator>=2.0; extra == 'all'
Requires-Dist: fastapi<1.0,>=0.142.1; extra == 'all'
Requires-Dist: opentelemetry-api>=1.26; extra == 'all'
Requires-Dist: opentelemetry-sdk>=1.26; extra == 'all'
Requires-Dist: prometheus-client>=0.25.0; extra == 'all'
Requires-Dist: pydantic-settings>=2.14.0; extra == 'all'
Requires-Dist: pydantic>=2.8; extra == 'all'
Requires-Dist: pyjwt[crypto]>=2.10.1; extra == 'all'
Requires-Dist: python-multipart>=0.0.20; extra == 'all'
Requires-Dist: pyyaml>=6.0; extra == 'all'
Requires-Dist: redis>=5.0; extra == 'all'
Requires-Dist: rich>=13.0; extra == 'all'
Requires-Dist: sqlalchemy>=2.0; extra == 'all'
Requires-Dist: starlette<2.0,>=1.3.1; extra == 'all'
Requires-Dist: typer>=0.12; extra == 'all'
Requires-Dist: uvicorn>=0.30; extra == 'all'
Provides-Extra: api
Requires-Dist: bcrypt>=4.0; extra == 'api'
Requires-Dist: email-validator>=2.0; extra == 'api'
Requires-Dist: fastapi<1.0,>=0.142.1; extra == 'api'
Requires-Dist: prometheus-client>=0.25.0; extra == 'api'
Requires-Dist: pydantic-settings>=2.14.0; extra == 'api'
Requires-Dist: pydantic>=2.8; extra == 'api'
Requires-Dist: pyjwt[crypto]>=2.10.1; extra == 'api'
Requires-Dist: python-multipart>=0.0.20; extra == 'api'
Requires-Dist: sqlalchemy>=2.0; extra == 'api'
Requires-Dist: starlette<2.0,>=1.3.1; extra == 'api'
Requires-Dist: uvicorn>=0.30; extra == 'api'
Provides-Extra: cli
Requires-Dist: click>=8.0; extra == 'cli'
Requires-Dist: rich>=13.0; extra == 'cli'
Requires-Dist: typer>=0.12; extra == 'cli'
Provides-Extra: dev
Requires-Dist: alembic>=1.13; extra == 'dev'
Requires-Dist: bandit>=1.7; extra == 'dev'
Requires-Dist: bcrypt>=4.0; extra == 'dev'
Requires-Dist: click>=8.0; extra == 'dev'
Requires-Dist: email-validator>=2.0; extra == 'dev'
Requires-Dist: fastapi<1.0,>=0.142.1; extra == 'dev'
Requires-Dist: httpx2>=0.1; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mkdocs-material>=9.4; extra == 'dev'
Requires-Dist: mkdocs>=1.5; extra == 'dev'
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'dev'
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pip-audit>=2.7; extra == 'dev'
Requires-Dist: pydantic-settings>=2.14.0; extra == 'dev'
Requires-Dist: pydantic>=2.8; extra == 'dev'
Requires-Dist: pyjwt[crypto]>=2.10.1; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.6; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: pyyaml>=6.0; extra == 'dev'
Requires-Dist: redis>=5.0; extra == 'dev'
Requires-Dist: rich>=13.0; extra == 'dev'
Requires-Dist: ruff==0.16.6; extra == 'dev'
Requires-Dist: sqlalchemy>=2.0; extra == 'dev'
Requires-Dist: starlette<2.0,>=1.3.1; extra == 'dev'
Requires-Dist: typer>=0.12; extra == 'dev'
Requires-Dist: uvicorn>=0.30; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.4; extra == 'docs'
Requires-Dist: mkdocs>=1.5; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
Provides-Extra: otel
Requires-Dist: opentelemetry-api>=1.26; extra == 'otel'
Requires-Dist: opentelemetry-sdk>=1.26; extra == 'otel'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Provides-Extra: sql
Requires-Dist: alembic>=1.13; extra == 'sql'
Requires-Dist: sqlalchemy>=2.0; extra == 'sql'
Provides-Extra: yaml
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
Description-Content-Type: text/markdown

[![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
[![CI](https://github.com/saurabh2mishra/fluxrules/actions/workflows/ci.yml/badge.svg)](https://github.com/saurabh2mishra/fluxrules/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/saurabh2mishra/fluxrules/branch/main/graph/badge.svg)](https://codecov.io/gh/saurabh2mishra/fluxrules)

# FluxRules

FluxRules is a Python library for authoring rules and evaluating them against facts. It is loosely based on the concepts from Drools’ Phreak engine.

Use FluxRules to keep decision logic separate from application flow and evaluate rules consistently across facts.

## Core Capabilities

- Evaluate a ruleset against facts using the top-level Python API.
- Run rule evaluation through FluxRules’ Phreak engine in stateless or streaming mode.
- Validate rulesets before evaluation.
- Retrieve evaluation explanations
- Use optional HTTP endpoints for evaluation, validation, explanations, and health checks.

## Requirements

- Python 3.10+

## Installation

For local development, clone the repository and install the project with uv:

```bash
git clone https://github.com/fluxrules/fluxrules.git
cd fluxrules
uv sync --extra dev
```

This creates the project environment and installs FluxRules with its development
tools. To install the published package in a virtual environment managed by uv:

```bash
uv venv
uv pip install fluxrules
```

Install an optional extra in the checkout with `uv sync`, for example:

```bash
uv sync --extra api
uv sync --extra sql
uv sync --extra redis
uv sync --extra otel
uv sync --extra yaml
uv sync --extra cli
uv sync --extra docs
uv sync --extra all
```

For more details see the [installation guide](docs/installation.md).

## Your First Rule

```python
from fluxrules import Rule, evaluate

rule = Rule(
    name="high_value_transaction",
    conditions={"type": "condition", "field": "amount", "op": ">", "value": 5000},
    action="manual_review",
)

result = evaluate(rule, {"amount": 6000})

print(f"Fired rules: {result.fired_rules}")
print(f"Actions: {result.actions}")
```

**Output:**
```
Fired rules: [1]
Actions: ['manual_review']
```

Constructing a `Rule` does no I/O and `evaluate()` takes rules directly, so
there is nothing to configure to get an answer. `evaluate()` also accepts a list
of rules, or a named `Ruleset`.

## Using the engine directly

```python
from fluxrules import PhreakEngine, Rule

rule = Rule(
    name="high_value_transaction",
    domain="payments",
    conditions={"type": "condition", "field": "amount", "op": ">", "value": 5000},
    action="manual_review",
    priority=100,
)

engine = PhreakEngine()
engine.load_rules([rule])

for fact in ({"amount": 6000}, {"amount": 10}):
    result = engine.evaluate(fact)
    print(fact, "->", result.fired_rules, result.actions)
```

To persist a rule, call `rule.save()`, or pass `persist=True` when constructing
it. See the [persistence guide](docs/persistence.md).

## API Server (Optional)

```bash
uv sync --extra api
uvicorn fluxrules.api.app:create_app --factory --reload
```
see [API](/docs/api.md) for more detail.

## CLI (Optional)

```bash
uv sync --extra cli
fluxrules --help
```

## How it works

### Evaluation pipeline

Each call to `evaluate()` runs a fact dictionary through four stages:

```
facts (dict)
  │
  ├─ Alpha prefilter — rules whose required fields are absent are skipped
  │
  ├─ BitMaskLinker — bitmask check confirms all required segment fields are present
  │
  ├─ Per-rule condition evaluation — only linked rules are tested
  │
  └─ EvaluationResult — fired rule ids, actions, and optional explanation
```

A per-cycle leaf memo collapses repeated sub-expression evaluations within a
single call (~96% reduction on shared leaves in typical rulesets).

### What is borrowed from Drools Phreak

- **BitMaskLinker** — rules are "linked" only when the incoming fact contains all
  fields their conditions reference; unlinked rules are skipped entirely before
  any condition is tested.
- **SegmentNetwork** — rules are grouped by their condition field-set; only the
  segments whose fields appear in the changed fact are re-evaluated.
- **Dirty-segment tracking** (`LazyEvaluationMixin`) — in streaming mode,
  segments whose inputs have not changed since the last call are not re-evaluated.
- **Salience-ordered Agenda** — a `heapq`-backed agenda orders fired rules by
  priority; populated in streaming mode for ordered action dispatch.

### What is intentionally left out

- **No beta network / join nodes.** fluxrules evaluates one fact at a time.
  `enforce_single_fact_boundary` raises at load time if a rule references more
  than one fact type. Cross-fact rules use a separate `CrossFactEngine`.
- **No truth maintenance system (TMS).** There is no consequence retraction;
  each call is independent and produces no side-effects on engine state.
- **No tuple-based partial match memory.** Without multi-fact joins, there is
  nothing to accumulate between rule firings.

These omissions are deliberate. fluxrules is a **stateless scoring function**:
`evaluate(facts) → EvaluationResult`. It is designed for concurrent HTTP
deployments where each request is independent and shared mutable state between
calls is a liability, not a feature.

### Stateless vs. streaming mode

In the default stateless mode, every `evaluate()` call starts from a clean
engine state. This is safe to use across threads and processes with no
coordination.

`streaming_mode=True` adds cross-call caching for long-lived engine instances:
dirty-segment tracking and the salience agenda are preserved between calls so
that unchanged segments are not re-evaluated. Use streaming mode when you control
the engine lifecycle (e.g., a single-process event stream), and stateless mode
everywhere else.

## Documentation

[DOCS](https://saurabh2mishra.github.io/fluxrules/)

## Contributing

[CONTRIBUTING](CONTRIBUTING.md) & [CODE OF CONDUCT](CODE_OF_CONDUCT.md)

## Security

[SECURITY](SECURITY.md)

## License

[LICENSE](LICENSE)