Extension and Plugin Architecture¶
updatesupport keeps the core package domain-neutral. Domain-specific use cases
can layer on their own metrics, Q presets, compilers, and report profiles through
small extension packages.
The core package owns:
finite problem compilation
public/hidden support logic
built-in Q presets
row-metric and plugin protocols
sensitivity, refinement, and report machinery
Extension packages own domain vocabulary. For example,
updatesupport-finance supplies financial model-risk metrics and report
profiles without adding finance-specific concepts to core.
Installing Plugins¶
Install a plugin package directly:
# with pip
pip install updatesupport-finance
# with uv
uv add updatesupport-finance
Or install through a core extra:
# with pip
pip install "updatesupport[finance]"
# with uv
uv add "updatesupport[finance]"
Discovering Plugins¶
Plugins may register Python entry points under updatesupport.plugins.
import updatesupport as us
plugins = us.discover_plugins()
print([plugin.name for plugin in plugins])
Direct imports remain supported and are usually clearer in application code:
import updatesupport_finance as usf
metric = usf.expected_loss(pd="pd", lgd="lgd")
Plugin Descriptor¶
A plugin exposes an UpdateSupportPlugin descriptor:
import updatesupport as us
plugin = us.UpdateSupportPlugin(
name="finance",
version="0.1.0",
description="Financial model-risk extensions for updatesupport.",
metrics={"expected_loss": expected_loss},
q_presets={"portfolio_mix_shift": q_portfolio_mix_shift},
report_profiles={"model_risk": model_risk_report},
compilers={"portfolio": from_portfolio},
metadata=us.PluginMetadata(
package="updatesupport-finance",
homepage="https://github.com/nahuaque/updatesupport",
domain="financial-model-risk",
tags=("credit-risk", "expected-loss", "model-validation"),
min_updatesupport_version="0.1.1",
),
)
The descriptor is intentionally lightweight. It gives discovery, namespacing, and lookup, while each plugin keeps its own user-facing API.
Plugin SDK Checks¶
Core validates plugin descriptors before registration. A valid plugin has a non-empty slug-like name, optional string metadata, and callable values in each surface map.
report = us.validate_plugin(plugin)
report.raise_for_errors()
print(plugin.as_dict())
Use this in plugin tests so packaging errors fail before release:
def test_plugin_contract():
assert us.validate_plugin(plugin).ok
register_plugin(...) protects duplicate plugin names by default:
us.register_plugin(plugin)
# Only use replace=True intentionally, for example in an interactive notebook
# after reloading local plugin code.
us.register_plugin(plugin, replace=True)
Entry-point discovery uses the same validation and duplicate-name checks. A package that exposes a plugin entry point should keep the entry point name and the descriptor name aligned:
[project.entry-points."updatesupport.plugins"]
finance = "updatesupport_finance:plugin"
Row Metrics¶
Plugins can provide row-level target abstractions with RowMetric.
metric = us.row_metric(
"expected_loss_rate",
lambda row: row["pd"] * row["lgd"],
columns=("pd", "lgd"),
description="expected loss rate",
)
grouped = us.from_dataframe(
rows,
public=["product", "region"],
hidden=["product", "region", "channel"],
target=metric,
weight="ead",
)
This compiles to the same GroupedProblem representation as a target column.
The plugin only controls how the row-level target value is computed.
First Plugin: updatesupport-finance¶
updatesupport-finance provides:
expected_loss(...)expected_loss_amount(...)default_rate(...)loss_given_default(...)finance_sensitivity_grid(...)from_portfolio(...)model_risk_report(...)certify_portfolio_segmentation(...)q_portfolio_mix_shift(...)q_exposure_weighted_tv(...)q_factor_exposure_shift(...)q_regional_concentration_shift(...)
Example:
import updatesupport_finance as usf
report = usf.model_risk_report(
portfolio,
public=["product", "region", "fico_band", "ltv_band"],
hidden=[
"product",
"region",
"fico_band",
"ltv_band",
"broker_channel",
"employment_type",
"vintage",
],
metric=usf.expected_loss(pd="pd", lgd="lgd"),
exposure="ead",
candidate_refinements=["broker_channel", "employment_type", "vintage"],
q="saturated",
)
print(report.to_markdown())
The finance plugin intentionally leads with saturated fixed-public-law stress because it is the easiest model-risk control to explain: it requires the public segmentation, retained internal refinements, and supplied metric, but no hand-chosen radius. Radius-based mix, TV, factor-exposure, and regional concentration presets are better treated as sensitivity scenarios with a documented governance rationale.