handlers.trigger

Unified trigger/condition evaluation for event handlers.

A handler's "trigger" decides whether an incoming event should actually run the handler. It is configured per FeatureConfig under the trigger key and unifies what used to be spread across the class-level conditions attribute and the various bespoke condition checks inside individual handlers.

A trigger is a dict of scenarios, all AND-combined - every present scenario must hold for the handler to run. Supported scenarios:

events          list[str]   - event types. Evaluated *before* the fact sheet
                              preload via EventHandler.get_event_types();
                              overrides the class-level ``conditions``.
fs_types        list[str]   - fact sheet types (pre-preload, see get_fs_types()).
field_updated   str         - only run on a FactSheetFieldUpdatedEvent whose
                              path is "/<field_updated>".
match           dict        - {field: value} equality, AND-combined. A string
                              value prefixed with "!" negates (field must NOT
                              equal the value).
field_value_in  dict        - {field: [values]} membership, AND-combined.
event_match     dict        - {event_key: value} against the top-level keys of the
                              *event* payload (not the fact sheet), AND-combined,
                              e.g. {"userId": "<id>"}. A string value prefixed
                              with "!" negates; a list value means membership.
                              Event-only, so it doesn't force a full fact sheet
                              load (unlike an equivalent ``code`` trigger).
creator_match   dict        - {user_key: value} against the user who *created* the
                              fact sheet ("id", "displayName", "email"), read from
                              the fact sheet's first log entry. Same value syntax as
                              event_match. Needed where the event's ``userId`` is
                              ambiguous: LeanIX system users (e.g. "System user
                              'trc-data-digest'" and "System user 'reference-data'")
                              share one user id and differ only by displayName, which
                              isn't in the event. Costs one log lookup (retried briefly
                              if the entry isn't readable yet), so it is evaluated
                              last; combine it with fs_types / event_match to keep
                              lookups rare. An event that already carries the creator
                              as ``event["creator"]`` (initial sync) skips the lookup.
code           str | list  - arbitrary Python with ``fs`` and ``event`` in
                              scope. A string is evaluated as
                              ``lambda fs, event: <string>``; a list of lines
                              is evaluated as a function body and MUST contain
                              a ``return``. A truthy result means the trigger
                              matches.

events and fs_types are cheap and event-only, so they are consumed by EventHandler.get_event_types()/get_fs_types() and filtered before the expensive fact sheet preload. The remaining scenarios need the loaded fact sheet and are evaluated by matches(event, fs) after preload.

Shorthand: a bare string trigger is treated as {"code": <string>}.

A top-level list is reserved for a future OR-of-triggers grammar and currently raises NotImplementedError. (Code that needs multiple lines goes under the code key as a list, not at the top level.)

class Trigger:
Trigger(spec)
POST_PRELOAD_KEYS = ('field_updated', 'match', 'field_value_in', 'event_match', 'code', 'creator_match')
CREATOR_LOOKUP_ATTEMPTS = 3
CREATOR_LOOKUP_DELAY_SECONDS = 1.0
spec
events
fs_types
def matches(self, event, fs, skip=()):

Evaluate the post-preload scenarios (field_updated, match, field_value_in, event_match, code, creator_match), AND-combined. Returns True if the handler should run. creator_match goes last: it is the only one that calls LeanIX.

events/fs_types are intentionally not re-checked here: they are cheap, event-only, and already filtered before the fact sheet preload via EventHandler.get_event_types()/get_fs_types().

skip names scenarios to leave out, e.g. InitialSync skips "field_updated", since its synthetic events stand for "every field as it is now".

def referenced_fields(self):

Statically extract the fact-sheet field keys this trigger reads in matches(), so the event loop can fold them into the lean-load field set.

Returns a list of field-name strings drawn from the 'match' and 'field_value_in' scenarios ('field_updated' reads the event path, not a fact-sheet field).

Returns None if the trigger cannot be introspected statically - i.e. it has a 'code' scenario running arbitrary Python. The event loop treats None as "load the full fact sheet".