orchestrata_ultimate.postman.routing_rules

Reference › orchestrata_ultimate › postman › routing_rules

The content-based routing rules of the broker, SPEC-002 story US-005 (REQ-002-US-005-012 to -020, REQ-002-FR3-009). RoutingRulesEngine implements IRoutingRules from interfaces and lets an operator define rules such as "messages from this sender to this topic also go to these agents". Matching is delegated to filter. No other module imports it.

add_rule stores a RoutingRule (from types) by its id and raises InvalidRoutingRuleError when the rule has no name or no destination. remove_rule, update_rule and list_rules manage the set. evaluate(envelope, payload) sorts the rules by priority number and then name, skips disabled ones, and for each rule whose filter matches adds its destinations to the result; a rule with stop_on_match ends the walk. The result is the combined destination list, which can be empty.

At a glance

Source src/orchestrata_ultimate/postman/routing_rules.py
Lines of code 187
Classes 1
Functions 0
Imports (internal) orchestrata_ultimate.core.base.traces, orchestrata_ultimate.postman.exceptions, orchestrata_ultimate.postman.filter, orchestrata_ultimate.postman.interfaces, orchestrata_ultimate.postman.types
Imported by none
Tests that import it tests/postman/test_coverage_routing_rules.py, tests/postman/test_postman_signatures.py, tests/postman/test_routing_rules.py

Class diagram

Class diagram orchestrata_ultimate.postman.routing_rules

Dependency diagram

Dependency diagram orchestrata_ultimate.postman.routing_rules

API

Classes

RoutingRulesEngine

Bases: IRoutingRules · Decorators: traces(spec='002', story='US-005', reqs=['REQ-002-US-005-012', 'REQ-002-US-005-013', 'REQ-002-US-005-014', 'REQ-002-US-… · Line 60

Implementation of IRoutingRules.

Method Signature Summary
__init__ __init__(message_filter: IMessageFilter | None = None) -> None Initialize the routing rules engine.
add_rule async add_rule(rule: RoutingRule) -> str Add a routing rule.
remove_rule async remove_rule(rule_id: str) -> bool Remove a routing rule.
update_rule async update_rule(rule_id: str, rule: RoutingRule) -> bool Update an existing routing rule.
list_rules async list_rules() -> list[RoutingRule] List all routing rules.
evaluate async evaluate(envelope: MessageEnvelope, payload: MessagePayload) -> list[str] Evaluate routing rules against a message.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/postman/routing_rules.py

# mypy: ignore-errors
"""
Routing Rules Engine.

Implements IRoutingRules interface for managing and evaluating routing rules.
"""

# Spec: 002 - Postman Message Broker

import asyncio
import logging
from typing import TYPE_CHECKING

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.postman.exceptions import InvalidRoutingRuleError
from orchestrata_ultimate.postman.interfaces import IMessageFilter, IRoutingRules
from orchestrata_ultimate.postman.types import (
    MessageEnvelope,
    MessagePayload,
    RoutingRule,
)

logger = logging.getLogger(__name__)

# Try to import our filter implementation, but provide a fallback. Resolved
# via TYPE_CHECKING so pyright always type-checks against the real
# postman.filter.MessageFilter (a single canonical type) instead of also
# seeing the runtime-only fallback stub below, whose narrower
# `compile_filter(...) -> None` doesn't structurally match
# IMessageFilter.compile_filter's `-> Callable` — that mismatch only matters
# if the fallback branch is ever actually reached (missing module), which
# never happens for pyright's own static analysis of this in-package import.
if TYPE_CHECKING:
    from orchestrata_ultimate.postman.filter import MessageFilter
else:
    try:
        from orchestrata_ultimate.postman.filter import MessageFilter
    except ImportError:
        # If filter module is not available, define a placeholder
        class MessageFilter(IMessageFilter):
            def matches(self, envelope, payload, criteria) -> bool:
                raise NotImplementedError("MessageFilter not available")

            @traces(spec="002", story="FR-003", reqs=["REQ-002-FR3-009"])
            def compile_filter(self, criteria) -> None:
                raise NotImplementedError("MessageFilter not available")


@traces(
    spec="002",
    story="US-005",
    reqs=[
        "REQ-002-US-005-012",
        "REQ-002-US-005-013",
        "REQ-002-US-005-014",
        "REQ-002-US-005-015",
        "REQ-002-US-005-018",
    ],
)
class RoutingRulesEngine(IRoutingRules):
    """Implementation of IRoutingRules."""

    def __init__(self, message_filter: IMessageFilter | None = None) -> None:
        """
        Initialize the routing rules engine.

        Args:
            message_filter: Optional IMessageFilter instance.
                If not provided, a default MessageFilter will be created.
        """
        self._rules: dict[str, RoutingRule] = {}
        self._filter = message_filter or MessageFilter()
        self._lock = asyncio.Lock()

    @traces(
        spec="002",
        story="US-005",
        reqs=["REQ-002-US-005-012", "REQ-002-US-005-017"],
    )
    async def add_rule(self, rule: RoutingRule) -> str:
        """
        Add a routing rule.

        Args:
            rule: Routing rule to add.

        Returns:
            The rule_id of the added rule.

        Raises:
            InvalidRoutingRuleError: If the rule is invalid.
        """
        async with self._lock:
            # Basic validation
            if not rule.name:
                raise InvalidRoutingRuleError("Rule must have a non‑empty name")
            if not rule.destinations:
                raise InvalidRoutingRuleError(
                    "Rule must specify at least one destination"
                )
            self._rules[rule.rule_id] = rule
            return rule.rule_id

    @traces(spec="002", story="US-005", reqs=["REQ-002-US-005-014"])
    async def remove_rule(self, rule_id: str) -> bool:
        """
        Remove a routing rule.

        Args:
            rule_id: ID of the rule to remove.

        Returns:
            True if the rule was found and removed, False otherwise.
        """
        async with self._lock:
            if rule_id in self._rules:
                del self._rules[rule_id]
                return True
            return False

    @traces(spec="002", story="US-005", reqs=["REQ-002-US-005-013"])
    async def update_rule(self, rule_id: str, rule: RoutingRule) -> bool:
        """
        Update an existing routing rule.

        Args:
            rule_id: ID of the rule to update.
            rule: Updated routing rule (its rule_id field may differ; it will be forced to rule_id).

        Returns:
            True if the rule was found and updated, False otherwise.
        """
        async with self._lock:
            if rule_id not in self._rules:
                return False
            # Ensure the stored rule uses the given rule_id
            rule.rule_id = rule_id
            self._rules[rule_id] = rule
            return True

    @traces(spec="002", story="US-005", reqs=["REQ-002-US-005-015"])
    async def list_rules(self) -> list[RoutingRule]:
        """
        List all routing rules.

        Returns:
            List of all stored routing rules.
        """
        async with self._lock:
            return list(self._rules.values())

    @traces(
        spec="002",
        story="US-005",
        reqs=[
            "REQ-002-FR3-009",
            "REQ-002-US-005-018",
            "REQ-002-US-005-019",
            "REQ-002-US-005-020",
        ],
    )
    async def evaluate(
        self,
        envelope: MessageEnvelope,
        payload: MessagePayload,
    ) -> list[str]:
        """
        Evaluate routing rules against a message.

        Args:
            envelope: Message envelope.
            payload: Message payload.

        Returns:
            List of destination agent IDs resulting from matching rules.
        """
        # Sort rules by priority (lower priority value = higher priority)
        sorted_rules = sorted(self._rules.values(), key=lambda r: (r.priority, r.name))
        destinations = []
        for rule in sorted_rules:
            if not rule.enabled:
                continue
            if self._filter.matches(envelope, payload, rule.filter_criteria):
                destinations.extend(rule.destinations)
                if rule.stop_on_match:
                    break
        return destinations