# Dynamic Hooks and Filters Extension

The Dynamic Hooks/Filters extension allows registering callbacks from YAML instead of hardcoding registrations in plugin `load(app)` functions.

## Overview

Key behavior:

1. Registers itself on `post_argument_parsing`
2. Reads hook/filter config from YAML
3. Validates each callback against discovery registries
4. Validates configured parameters against the callback's Pydantic parameter model
5. Registers validated callbacks into Cement's hook/filter managers

## Configuration Model

### Workflow-aware structure (recommended)

```yaml
vipr:
  inference:  # also works for other workflow sections
    hooks:
      HOOK_NAMESPACE:
      - class: package.module.ClassName
        method: method_name
        enabled: true
        weight: 0
        parameters:
          key: value

    filters:
      FILTER_NAMESPACE:
      - class: package.module.ClassName
        method: method_name
        enabled: true
        weight: 0
        parameters:
          key: value
```

### Legacy fallback (still supported)

```yaml
vipr:
  hooks: {}
  filters: {}
```

The extension aggregates hooks/filters across all `vipr.<workflow>.hooks|filters` sections and falls back to `vipr.hooks|filters` when no workflow sections are found.

## Required Fields

| Field | Required | Description |
|---|---|---|
| `class` | yes | Fully-qualified class path |
| `method` | yes | Method name on that class |
| `enabled` | no | Defaults to `true` |
| `weight` | no | Defaults to `0` (lower runs earlier) |
| `parameters` | no | Must match callback's Pydantic parameter model |

Only `class + method` references are accepted. Standalone functions are not supported.

## Examples

### Example 1: Register a built-in normalizer filter

Decorator declaration (in code):

```python
@discover_filter('INFERENCE_PREPROCESS_PRE_FILTER', enabled_in_config=False, weight=-10)
def normalize_filter(self, data, **kwargs):
    return data
```

YAML activation:

```yaml
vipr:
  inference:
    filters:
      INFERENCE_PREPROCESS_PRE_FILTER:
      - class: vipr.plugins.normalizers.log_normalizer.LogNormalizer
        method: normalize_filter
        enabled: true
        weight: -10
```

### Example 2: Register a hook with validated parameters

Decorator declaration (in code):

```python
class MarginalDistributionsParams(BaseModel):
    min_bins: int = 100

@discover_hook(
    'INFERENCE_POSTPROCESS_PRE_PRE_FILTER_HOOK',
    parameters=MarginalDistributionsParams,
    enabled_in_config=False
)
def create_marginal_distributions(self, app, data=None, **kwargs):
    ...
```

YAML activation:

```yaml
vipr:
  inference:
    hooks:
      INFERENCE_POSTPROCESS_PRE_PRE_FILTER_HOOK:
      - class: vipr_reflectometry.flow_models.postprocess.marginal_distributions.MarginalDistributions
        method: create_marginal_distributions
        enabled: true
        weight: 0
        parameters:
          min_bins: 120
```

### Example 3: Multiple callbacks with weights

```yaml
vipr:
  inference:
    hooks:
      INFERENCE_POSTPROCESS_PRE_PRE_FILTER_HOOK:
      - class: vipr_reflectometry.flow_models.postprocess.basic_corner_plot.BasicCornerPlot
        method: create_basic_corner_plot
        enabled: true
        weight: 0

      - class: vipr_reflectometry.flow_models.postprocess.cluster.clustering.hook.ClusterHook
        method: cluster_posterior_samples
        enabled: true
        weight: 1
        parameters:
          method: gmm
          n_components: 5
          n_init: 10
```

## Security and Validation

### 1. Registry validation

A callback must exist in discovered hooks or filters (`@discover_hook` / `@discover_filter`).

### 2. Parameter schema validation

If YAML provides `parameters`, the target callback must expose an attached Pydantic model via discovery decorators. Unknown or invalid fields are rejected.

### 3. Content validation

The extension applies pattern checks to parameter values (for example shell-injection/path-traversal/python-injection patterns).

## Plugin Availability Notes

- Internal callbacks under `vipr.plugins.<name>...` require the plugin to be enabled.
- Internal plugin config format is top-level:

```yaml
plugin.normalizers:
  enabled: true
```

- For external packages (for example `vipr_reflectometry...`), ensure the package is installed and importable in the active environment.

## Troubleshooting

### "Callback not in registry"

The configured `class + method` is not discovered.  
Fix: add correct `@discover_hook`/`@discover_filter` declaration or update YAML path/method.

### "Parameter validation failed"

YAML `parameters` do not match the callback's Pydantic parameter model.  
Fix: align parameter names/types with the discovered schema.

### "Could not import class.method"

Import path is wrong or package/plugin is unavailable.  
Fix: verify module path and install/enable the plugin.

### "Dangerous pattern in parameter"

A parameter value matched a blocked security pattern.  
Fix: review and sanitize the value.

## Best Practices

1. Use class-method callbacks only.
2. Keep callback parameters defined via Pydantic models.
3. Use workflow-local config (`vipr.inference.hooks|filters`) instead of global legacy structure.
4. Use weights intentionally and document ordering in configs.
5. Prefer maintained example configs from plugin repositories when possible.

## See Also

- [Inference Plugin](../plugins/built-in/inference.md)
- [Normalizers Plugin](../plugins/built-in/normalizers.md)
- {py:mod}`vipr.ext.dynamic_hooks_filters.extension`
- {py:mod}`vipr.plugins.discovery.decorators`
