Hooks and plugins

Hooks let you add domain-specific behaviour to the catalog operation lifecycle without modifying ogcat core.

Hook protocols

Each hook is a plain Python class that implements one or more of the following methods. You only need to implement the methods you care about.

Method

When it runs

before_validate_metadata(context)

Before schema validation. Mutate context.user_metadata here.

after_validate_metadata(context, report)

After schema validation. Inspect the validation report.

resolve_artifact_locator(context)

After the locator is proposed. Replace or extend context.planned_locators.

extract_metadata(context)

During derived metadata collection. Return a dict to merge into context.derived_metadata.

before_record_write(context)

Just before writing the record. Last chance to mutate metadata.

after_record_write(context)

Just after the record is written. Register side-effects here.

before_commit(context)

Before committing the transaction.

after_commit(context)

After committing the transaction (errors here become warnings).

on_error(context, error)

When the operation fails.

on_rollback(context, error)

After rollback has run.

Registering hooks

Pass a plugin registry when creating or opening a catalog:

from ogcat import Catalog, CatalogSpec, PluginRegistry
from ogcat.hooks import OperationContext


class TitleFromFilenameHook:
    """Set title from the source filename when the caller did not supply one."""

    def before_validate_metadata(self, context: OperationContext) -> None:
        if context.source_path is not None:
            context.user_metadata.setdefault("title", context.source_path.stem)


spec = CatalogSpec(catalog_name="files")
plugins = PluginRegistry([TitleFromFilenameHook()])
catalog = Catalog.create("./my-catalog", spec, plugins=plugins)

For ad hoc notebook workflows, you can also pass any iterable of hook objects directly as hooks= or plugins=:

catalog = Catalog.open("./my-catalog", hooks=[TitleFromFilenameHook()])

Catalog.open() and Catalog.create() validate hook inputs immediately. Each supplied hook object must implement at least one supported hook method listed above, and any matching hook method must be callable. This stricter validation means inert sentinel objects and no-op objects with no hook methods are rejected instead of being silently skipped.

You can also register hooks after catalog creation:

catalog.hook_manager.register(TitleFromFilenameHook())

Metadata extractor hooks

Implement extract_metadata to add derived metadata:

class ChecksumHook:
    """Compute a SHA-256 checksum and store it in derived metadata."""

    def extract_metadata(self, context: OperationContext) -> dict | None:
        if context.source_path is None:
            return None
        import hashlib
        digest = hashlib.sha256(context.source_path.read_bytes()).hexdigest()
        return {"sha256": digest}

OperationContext

OperationContext is the mutable object passed to every hook. The most commonly used fields are:

  • context.user_metadata — mutable before validation

  • context.derived_metadata — mutable during extraction

  • context.source_path — source path when available

  • context.planned_locators — list of locators to be resolved

  • context.operation_type — name of the current operation

Warnings

Hooks can record non-fatal warnings with context.add_warning():

context.add_warning("title was derived from filename", hook_name="TitleFromFilenameHook")

Warnings appear in after_commit errors and are attached to the operation context. They do not prevent the record from being written.

A complete example

See the intermediate tutorial for a full worked example including a custom extractor hook and smoke tests.