This project implements a flexible rule engine exposed via a FastAPI REST API. The core engine executes decision logic defined in graph-based data structures, allowing for different sets of rules (e.g., planning permission, discount eligibility) to be loaded and evaluated dynamically.
The primary example implemented is a set of rules for determining UK planning permission requirements for fences, gates, and walls, based on criteria like location, height, and specific planning constraints. The system is designed with separation of concerns, testability, and containerized deployment in mind.
- REST API: FastAPI endpoint (
/planning) to evaluate planning logic. - Data Validation: Pydantic models for robust request/response validation and OpenAPI documentation.
- Rule Engine: Generic Python engine (
engine.py) executes logic defined as a graph (Python dictionary). - Decoupled Logic: Logic graph definitions are stored separately (e.g.,
planning_logic.py,discount_logic.py). - Factory Pattern: A factory (
factory.py) loads the appropriate logic graph on demand, decoupling the API/service layer from the definition source. - Configurable Logic: Graphs include configuration for start nodes and default outcomes for error handling.
- Structured Logging: Centralized logging configuration (
logging_config.py) outputs to both console and a rotating file. - Containerization: Dockerfile provided for building a container image.
- Persistent Logs: Docker volume mounting allows logs to persist outside the container lifecycle.
- Testing: Includes unit tests for the engine/factory and integration tests for the API endpoint using
pytestandTestClient. - Modular Structure: Organized by core engine logic (
src) and API features (api/planning).
- Language: Python 3.10+
- Web Framework: FastAPI
- Data Validation: Pydantic
- ASGI Server: Uvicorn
- Testing: Pytest, FastAPI TestClient, HTTPX
- Containerization: Docker
- Standard Libraries:
logging,importlib,os,sys,pathlib
- Python (3.10 or later recommended)
pip(Python package installer)- Docker Desktop or Docker Engine/CLI
-
Clone the Repository:
git clone https://github.com/jryusuf/rule_engine_project cd rule_engine_project -
Create and Activate Virtual Environment:
# Linux/macOS python3 -m venv venv source venv/bin/activate # Windows python -m venv venv .\venv\Scripts\activate
-
Install Dependencies:
pip install -r requirements.txt
To run the FastAPI application locally for development:
-
Ensure your virtual environment is activated.
-
Navigate to the project root directory (
rule_engine_project/). -
Run Uvicorn:
uvicorn api.server:app --reload --app-dir .--reload: Enables auto-reloading on code changes.--app-dir .: Helps Uvicorn find theapipackage correctly from the root.
-
The API will typically be available at
http://127.0.0.1:8000. -
Access interactive documentation:
- Swagger UI:
http://127.0.0.1:8000/docs - ReDoc:
http://127.0.0.1:8000/redoc
- Swagger UI:
- Ensure your virtual environment is activated and development dependencies are installed.
- Navigate to the project root directory (
rule_engine_project/). - Run pytest:
Or for more verbose output:
pytest
pytest -v
The application is designed to be deployed as a Docker container.
-
Build the Docker Image: Navigate to the project root directory and run:
docker build -t planning-engine-api .(Replace
planning-engine-apiwith your desired image tag). -
Run the Docker Container: To run the container and map the API port and log volume:
# Option A: Using a named volume for logs (Recommended) docker volume create api_logs # Optional: create volume beforehand docker run -d -p 8001:8000 \ -v api_logs:/app/logs \ --name planning-api-container \ planning-engine-api # Option B: Using a host directory mount for logs # mkdir logs # Create logs directory in project root first if it doesn't exist # docker run -d -p 8001:8000 \ # -v "$(pwd)/logs":/app/logs \ # --name planning-api-container \ # planning-engine-api
-d: Run in detached mode.-p 8001:8000: Map port 8001 on the host to port 8000 in the container. Access the API viahttp://localhost:8001.-v api_logs:/app/logsor-v "$(pwd)/logs":/app/logs: Mounts a volume to persist logs written to/app/logsinside the container.--name planning-api-container: Assigns a name to the container.planning-engine-api: The name of the image to run.
-
Accessing the API:
- Root:
http://localhost:8001/ - Docs:
http://localhost:8001/docs - Endpoint:
POST http://localhost:8001/planning/evaluate
- Root:
-
Managing the Container:
- View Logs:
docker logs planning-api-container - Stop:
docker stop planning-api-container - Remove:
docker rm planning-api-container(Logs in the volume will persist)
- View Logs:
- Separation of Concerns:
- The core rule engine logic (
src/rule_engine_core) is kept separate from the API framework (api). This makes the engine potentially reusable in other contexts (e.g., CLI tool, different web framework). - Within the API, concerns are further separated into Routers (HTTP handling), Services (business logic orchestration), and Models (data structure definition).
- The core rule engine logic (
- Feature-Based API Structure: The
apidirectory is organized by feature (e.g.,api/planning/). This promotes modularity and makes it easier to add new, distinct rule evaluation endpoints in the future by creating new feature folders. - Data-Driven Logic Graph:
- The core decision logic is defined as Python dictionaries (
planning_logic.py,discount_logic.py) representing a graph structure. This separates the logic definition from the execution code. - Nodes have types (
start,decision,terminal). - Decision nodes use Python
lambdafunctions for concise condition evaluation against input data. - Connections define the flow based on decision outcomes.
- A
configsection within the graph specifies the start node and a default outcome for error handling.
- The core decision logic is defined as Python dictionaries (
- Factory Pattern (
factory.py):- A
LogicFactoryis used to load the appropriate logic graph dictionary based on a name (e.g., "planning"). - This decouples the Service layer (which needs a graph) from the specifics of how and from where the graph is loaded (currently Python modules, but could be changed to JSON, YAML, database within the factory).
- Includes caching to improve performance by avoiding redundant loading.
- A
- Generic Engine (
engine.py):- The
execute_logic_graph_with_defaultfunction is designed to be generic. It takes any valid graph dictionary and input data and traverses the defined logic. - It handles different node types and evaluates conditions dynamically.
- The
- Centralized Logging (
logging_config.py):- Logging is configured once using a dedicated utility function called at application startup (
api/server.py). - Outputs to both console (for
docker logs) and a rotating file (/app/logs/api.loginside the container). - File logging uses a volume mount in Docker to persist logs across container restarts/recreations.
- Logging is configured once using a dedicated utility function called at application startup (
- Testing Strategy:
- Unit Tests (
tests/test_engine.py,tests/test_factory.py): Test the core engine and factory components in isolation. - Integration Tests (
tests/test_api_planning.py): Use FastAPI'sTestClientto test the full request/response cycle for the API endpoint, including routing, validation, service interaction, and engine execution, without needing a running server or Docker.
- Unit Tests (
- Containerization (Docker):
- Provides a consistent, isolated environment for running the application.
- Simplifies deployment and dependency management.
- Uses multi-stage builds (implicitly by copying requirements first) to optimize build caching.
- Configured for persistent logging using volumes.