correspond.routing

Deciding what a message is about: a transparent rule chain.

The rules belong to the caller; correspond runs them in a fixed order and reports which rule decided and why.

  1. Bindings, pattern target. A pattern is a conversation-reference glob (* and […]; ? always starts the conditions), optionally followed by ?field=glob&… conditions on the message (%-escapes decoded, + kept): a native field (labels, state, …), author (handle, native id or address) or grade. A pattern without wildcards also matches the conversations under it, so github:example/app matches github:example/app#12.

  2. Thread continuity: threads maps a thread root, a replied-to message id or a conversation reference to the target it already belongs to.

  3. Metadata rules: callables message -> target | None, tried in order (metadata_rule() builds one from field=glob conditions).

  4. The classifier: one optional callable, last, for what rules cannot see. It returns a target, (target, reason), or None.

Nothing matched: route returns None and the message is unrouted. What that means (an operator queue, a drop) is the caller’s decision. A condition on a field the channel’s messages never carry (?label= where GitHub messages carry labels) never matches; check_binding() reports that when bindings are loaded.

>>> from correspond.testing import demo_message
>>> message = demo_message(conversation="github:example/app#12", labels=["partner:ada"])
>>> route(message, bindings={"github:example/app?labels=partner:*": "subject:app"})
RouteDecision(target='subject:app', rule='binding', reason='github:example/app?labels=partner:* matched github:example/app#12')
>>> route(message, bindings={"github:example/other": "subject:other"}) is None
True
correspond.routing.MESSAGE_FIELDS = ('author', 'grade')

Condition fields every message has, whatever its channel’s native fields.

correspond.routing.RULES = ('binding', 'thread', 'metadata', 'classifier')

The order the chain runs in.

class correspond.routing.RouteDecision(*, target: str, rule: str, reason: str)[source]

Where a message goes, which rule decided, and why.

to_dict() dict[source]

JSON-ready.

correspond.routing.binding_matches(pattern: str, message: Message) str | None[source]

The reason pattern matches message, or None.

correspond.routing.check_binding(pattern: str, *, registry: Mapping[str, Any] | None = None) list[str][source]

What would make a binding never match, found when bindings are loaded instead of by messages quietly going unrouted.

Reports a pattern without a channel, an unknown channel, and a condition on a field the channel’s messages never carry (its native_fields, plus author and grade). A channel written as a wildcard, or one that does not declare its fields (native_fields is None), is not checked for fields.

>>> from correspond.channels.github import GitHub
>>> check_binding("github:example/app?labels=bug", registry={"github": GitHub()})
[]
>>> check_binding("github:example/app?label=bug", registry={"github": GitHub()})[0].split(":")[0]
'the condition label=bug never matches'
correspond.routing.metadata_rule(target: str, *, name: str | None = None, **conditions: str) Callable[[Message], str | None][source]

A rule sending messages to target when every field=glob condition holds.

>>> from correspond.testing import demo_message
>>> rule = metadata_rule("urgent-queue", labels="priority:high")
>>> rule(demo_message(labels=["priority:high"])), rule(demo_message())
('urgent-queue', None)
correspond.routing.route(message: Message, *, bindings: Mapping[str, str] | Iterable[tuple[str, str]] | None = None, threads: Mapping[str, str] | None = None, rules: Iterable[Callable[[Message], str | None]] = (), classifier: Callable[[Message], str | tuple[str, str] | None] | None = None) RouteDecision | None[source]

Run the chain (bindings, thread continuity, metadata rules, classifier) and return the first decision, or None.