Skip to content

Jobs & hooks

Jobs schedule runs of their targets; hooks react to run outcomes; a retry policy decides when a failed attempt gets another.

Job

Job(**data: Any)

Bases: interloper.component.base.Component, interloper.operation.base.Workload

A materialization workload: the anchor of the job kind.

A job declares what to materialize (targets); concrete job classes add when: CronJob carries a cron trigger. Trigger fields are inert declarative intent: the framework carries them, and an operator (the scheduler) acts on them. The workload itself compiles to the same DAG that every other entry point executes.

A job also carries workload-level defaults: every relation the job holds is the default for the same relation on any target that resolves none, destinations included. The default applies where the job runs (operations), on a copy of the target; the target itself is never changed.

destinations

destinations = Relation(
    kind="destination",
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="block",
    name="destinations",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

icon

icon = 'carbon:event-schedule'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

key

key = 'job'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

kind

kind = 'job'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

model_config

model_config = {'arbitrary_types_allowed': True, 'ignored_types': (<class 'interloper.serializable.base.IgnoredDescriptor'>, <class 'interloper.component.relation.Relation'>)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

relations

relations = {
    "targets": Relation(
        kind=["source", "asset"],
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="detach",
        name="targets",
    ),
    "destinations": Relation(
        kind="destination",
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="block",
        name="destinations",
    ),
}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

targets

targets = Relation(
    kind=["source", "asset"],
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="detach",
    name="targets",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

state_model

Bases: pydantic.main.BaseModel

Machine-owned job state (see Component.state_model).

Timestamps are canonical timezone-aware ISO-8601 strings; the scheduler compares them lexicographically in SQL, so they are validated here but never rewritten. last_run_status qualifies last_run_at — it reads as that timestamp's badge rather than a column of its own.

model_config

model_config = {}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

model_post_init

model_post_init(context: Any) -> None

Default id to a generated UUID if not provided.

Parameters:

Name Type Description Default
context Any

The pydantic validation context, unused here.

required

operations

operations() -> list[Operation]

The targets' operations, each target run with this job's bindings where it resolves none.

Returns:

Type Description
list[Operation]

Every operation the job's targets provide.

CronJob

CronJob(**data: Any)

Bases: interloper.job.base.Job

A job triggered by a cron expression.

The trigger is declarative intent the scheduler acts on: cron sets the cadence on the wall clock of timezone, and a job whose targets declare time partitioning covers a trailing window of partitions on every tick. Whether a job is partitioned is derived from its targets' catalog definitions, never stored.

The window is counted in partitions, not days: offset is how many partitions back from the current one it ends, and lookback how many it spans. With daily targets, the defaults (offset=1, lookback=1) mean "yesterday only" — the job timezone's yesterday. Hourly windows are always UTC-derived regardless of timezone (hour partition keys are UTC labels, see lookback). Each firing is a backfill over that window, and concurrency is how many of its partitions are in flight at once, newest first.

destinations

destinations = Relation(
    kind="destination",
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="block",
    name="destinations",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

key

key = 'cron_job'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

model_config

model_config = {'arbitrary_types_allowed': True, 'ignored_types': (<class 'interloper.serializable.base.IgnoredDescriptor'>, <class 'interloper.component.relation.Relation'>)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

relations

relations = {
    "targets": Relation(
        kind=["source", "asset"],
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="detach",
        name="targets",
    ),
    "destinations": Relation(
        kind="destination",
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="block",
        name="destinations",
    ),
}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

targets

targets = Relation(
    kind=["source", "asset"],
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="detach",
    name="targets",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

model_post_init

model_post_init(context: Any) -> None

Default id to a generated UUID if not provided.

Parameters:

Name Type Description Default
context Any

The pydantic validation context, unused here.

required

window

window(
    config: Mapping[str, Any],
    *,
    fires_at: datetime,
    granularity: TimeGranularity | None,
) -> TimePartitionWindow | None

The trailing window of partitions a firing of a stored job config covers.

The window is read on the job timezone's clock at the firing instant, so a daily job's "yesterday" is its timezone's yesterday; hourly windows normalize back to UTC (see TimePartitionWindow.lookback).

Parameters:

Name Type Description Default
config Mapping[str, Any]

The job's stored config payload. A missing lookback means the default (1) and an explicit null opts out of windows.

required
fires_at datetime

The firing instant, aware.

required
granularity TimeGranularity | None

The granularity the job's targets share, from their catalog definitions; None for an unpartitioned job.

required

Returns:

Type Description
TimePartitionWindow | None

The window, or None for an unpartitioned job or one whose

TimePartitionWindow | None

lookback is null.

zone

zone(config: Mapping[str, Any]) -> tzinfo

The timezone a stored job config evaluates its schedule in.

Stored configs are read as raw payloads, without constructing the job: a name that no longer resolves (after a tzdata change) degrades to UTC rather than failing whoever reads it.

Parameters:

Name Type Description Default
config Mapping[str, Any]

The job's stored config payload.

required

Returns:

Type Description
tzinfo

The configured zone, or UTC when it is unset or unknown.

JobState

Bases: pydantic.main.BaseModel

Machine-owned job state (see Component.state_model).

Timestamps are canonical timezone-aware ISO-8601 strings; the scheduler compares them lexicographically in SQL, so they are validated here but never rewritten. last_run_status qualifies last_run_at — it reads as that timestamp's badge rather than a column of its own.

model_config

model_config = {}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

RetryPolicy

Bases: pydantic.main.BaseModel

How many times a unit of work is attempted, and how long between attempts.

Declared on the component whose unit it governs: an operation's policy retries that operation's execution, a source's is the default for its assets, a job's retries that job's runs. A component declaring none is attempted once; there is no instance-wide default.

The policy carries numbers only. Whether a given error is worth another attempt is behaviour rather than configuration, and lives on retryable.

model_config

model_config = {}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

allows

allows(attempt: int) -> bool

Whether the budget covers an attempt.

Parameters:

Name Type Description Default
attempt int

The attempt number, counting the first execution as 1.

required

Returns:

Type Description
bool

True while the attempt is within the budget.

delay_before

delay_before(attempt: int) -> float

How long to wait before an attempt.

The growth is geometric from the second attempt (the first retry), capped at max_delay, then spread by jitter so that operations failing together do not retry in lockstep.

Parameters:

Name Type Description Default
attempt int

The attempt number, counting the first execution as 1.

required

Returns:

Type Description
float

The delay in seconds, never negative; 0.0 for the first attempt.

Hook

Hook(**data: Any)

Bases: interloper.component.base.Component

A reaction to events on watched components.

A hook declares what to observe (watches), which outcomes matter (events), and, like an asset's partitioning or a job's cron, the declaration is inert intent: the framework carries it, and an operator (the scheduler) evaluates events and calls fire. Concrete hook classes own the side effect:

class SlackHook(Hook):
    channel: str = InputField(description="Channel to notify")

    def fire(self, context: HookContext) -> None:
        post_message(self.channel, context.metadata)

The base hook is only an observer. Hooks that act on other components (TriggerHook) extend the vocabulary with a targets relation, so each class's definition advertises exactly what it acts on.

icon

icon = 'carbon:lightning'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

key

key = 'hook'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

kind

kind = 'hook'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

model_config

model_config = {'arbitrary_types_allowed': True, 'ignored_types': (<class 'interloper.serializable.base.IgnoredDescriptor'>, <class 'interloper.component.relation.Relation'>)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

relations

relations = {
    "watches": Relation(
        kind=["source", "asset", "job"],
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="detach",
        name="watches",
    )
}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

watches

watches = Relation(
    kind=["source", "asset", "job"],
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="detach",
    name="watches",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

state_model

Bases: pydantic.main.BaseModel

Machine-owned hook state (see Component.state_model).

Stamped by the operator on every firing: last_error carries the latest firing's error and a successful firing clears it, so the state alone tells whether a hook is failing.

model_config

model_config = {}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

fire

fire(context: HookContext) -> None

Execute this hook's reaction to an event.

Subclasses must override this method.

Parameters:

Name Type Description Default
context HookContext

What the hook knows about the event that triggered it: its identity, metadata, and the operator-injected capabilities.

required

Raises:

Type Description
NotImplementedError

If the subclass does not implement fire().

model_post_init

model_post_init(context: Any) -> None

Default id to a generated UUID if not provided.

Parameters:

Name Type Description Default
context Any

The pydantic validation context, unused here.

required

HookContext

Bases: pydantic.main.BaseModel

What a firing hook knows about the event that triggered it.

Carries the event's identity and metadata, plus the capabilities the operator injects: trigger creates a run for a component id, so trigger-style hooks stay free of any persistence dependency. A run event names its run and partition; a backfill event names its backfill and the range it covered. url is the subject's page in the app, when the deployment has a public URL, so a notification can link back to it.

model_config

model_config = {'arbitrary_types_allowed': True}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

HookState

Bases: pydantic.main.BaseModel

Machine-owned hook state (see Component.state_model).

Stamped by the operator on every firing: last_error carries the latest firing's error and a successful firing clears it, so the state alone tells whether a hook is failing.

model_config

model_config = {}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

TriggerHook

TriggerHook(**data: Any)

Bases: interloper.hook.base.Hook

Triggers a run of each target when a watched event matches.

The cascading-pipelines primitive: watch an upstream source or job, target the downstream workload. Execution goes through the operator's injected context.trigger capability, so the hook itself carries no persistence dependency.

The targets relation lives here, not on the base hook: only trigger-style hooks act on other components, so only their definitions advertise it.

key

key = 'trigger_hook'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

model_config

model_config = {'arbitrary_types_allowed': True, 'ignored_types': (<class 'interloper.serializable.base.IgnoredDescriptor'>, <class 'interloper.component.relation.Relation'>)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

relations

relations = {
    "watches": Relation(
        kind=["source", "asset", "job"],
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="detach",
        name="watches",
    ),
    "targets": Relation(
        kind=["source", "asset", "job"],
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="block",
        name="targets",
    ),
}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

targets

targets = Relation(
    kind=["source", "asset", "job"],
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="block",
    name="targets",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

watches

watches = Relation(
    kind=["source", "asset", "job"],
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="detach",
    name="watches",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

fire

fire(context: HookContext) -> None

Trigger a run for every target.

Parameters:

Name Type Description Default
context HookContext

The event context, whose trigger capability creates the run for each target's component id.

required

Raises:

Type Description
ConfigError

If the operator provided no trigger capability.

model_post_init

model_post_init(context: Any) -> None

Default id to a generated UUID if not provided.

Parameters:

Name Type Description Default
context Any

The pydantic validation context, unused here.

required

WebhookHook

WebhookHook(**data: Any)

Bases: interloper.hook.base.Hook

POSTs a JSON document describing the event to a URL.

The payload is a fixed metadata document — the event type, the watched component, the run — so receivers integrate against one stable shape.

key

key = 'webhook_hook'

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

model_config

model_config = {'arbitrary_types_allowed': True, 'ignored_types': (<class 'interloper.serializable.base.IgnoredDescriptor'>, <class 'interloper.component.relation.Relation'>)}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

relations

relations = {
    "watches": Relation(
        kind=["source", "asset", "job"],
        key="",
        many=True,
        optional=True,
        default=None,
        on_delete="detach",
        name="watches",
    )
}

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

watches

watches = Relation(
    kind=["source", "asset", "job"],
    key="",
    many=True,
    optional=True,
    default=None,
    on_delete="detach",
    name="watches",
)

One declared link from an owner component to the components that may fill it.

kind and key name the acceptable targets: kind names one or several component kinds, key (when non-empty) narrows to specific keys within those kinds, matched through ComponentIdentity.satisfies. many marks a relation that binds several components at once; optional marks one that may stay unbound or be left empty, and says nothing else: on_delete alone decides what deleting a bound target does, block refusing the deletion while this relation holds the target and detach letting it through and dropping the binding. default and has_fallback together describe a relation that can be resolved without an explicit binding.

A relation is also its own descriptor: Component._collect installs the stamped copy under the relation's name, so the class attribute reads as the declaration and the instance attribute as what is bound to it, and assignment rebinds it.

The declaration form the framework writes pairs the relation with the annotation of what fills it (destinations: list[Destination] = Relation("destination", many=True)). The TYPE_CHECKING __new__ is what makes that form check: the type checker reads a Pydantic field of the annotated type, so the constructor kwarg and the instance attribute are both typed, while the runtime sees a Relation (Pydantic ignores it, and Component._collect drops the annotation before fields are collected).

Two attributes are not declared but derived. name is stamped from the attribute the relation is declared under, and target is the class the Relation(cls) shorthand was written with, the class a fallback is built from; neither is a constructor argument. default (intended type Callable[[], Component] | None) and target (intended type type[Component] | None) are typed Any for the type checker, since Component is only imported under TYPE_CHECKING; both stay out of dumps.

fire

fire(context: HookContext) -> None

POST the event payload to the configured URL.

An error status from the receiver raises httpx2.HTTPStatusError.

Parameters:

Name Type Description Default
context HookContext

The event context, serialized into the POST payload.

required

model_post_init

model_post_init(context: Any) -> None

Default id to a generated UUID if not provided.

Parameters:

Name Type Description Default
context Any

The pydantic validation context, unused here.

required

HOOK_EVENT_TYPES

HOOK_EVENT_TYPES = (
    "run_completed",
    "run_failed",
    "backfill_completed",
    "backfill_failed",
)

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.