feat: add typed tier assurance guardrails
This commit is contained in:
parent
3d0f614f49
commit
c65a2f1ff9
13 changed files with 727 additions and 7 deletions
|
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||
from typing import Any, Literal
|
||||
|
||||
PricingModelStatus = Literal["active", "candidate", "retired"]
|
||||
AssuranceClaimKind = Literal["isolation", "availability", "retention", "performance"]
|
||||
ChargeComponentKind = Literal[
|
||||
"access",
|
||||
"setup",
|
||||
|
|
@ -40,6 +41,9 @@ _ALLOWED_PARAMETER_CLASSES = {
|
|||
"constrained",
|
||||
"provider",
|
||||
}
|
||||
_ALLOWED_ASSURANCE_KINDS = {"isolation", "availability", "retention", "performance"}
|
||||
_AXIS_MAXIMUMS = {"I": 3, "A": 4, "E": 4, "P": 4, "R": 4, "V": 4}
|
||||
_ALLOWED_ERASURE_MECHANISMS = {"row-deletion", "key-destruction"}
|
||||
|
||||
|
||||
def _money(value: str | int | float | Decimal | None) -> Decimal | None:
|
||||
|
|
@ -88,6 +92,20 @@ class TunableParameter:
|
|||
options: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AssuranceClaim:
|
||||
id: str
|
||||
kind: AssuranceClaimKind | str
|
||||
customer_wording: str
|
||||
minimum_levels: dict[str, int]
|
||||
delivering_service: str
|
||||
evidence_ref: str
|
||||
maximum_erasure_horizon_days: int | None = None
|
||||
provider_contract_ref: str | None = None
|
||||
resource_governor_ref: str | None = None
|
||||
erasure_mechanism: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PricingModel:
|
||||
id: str
|
||||
|
|
@ -104,6 +122,7 @@ class PricingModel:
|
|||
charge_components: tuple[ChargeComponent, ...] = ()
|
||||
commitments: tuple[Commitment, ...] = ()
|
||||
tunable_parameters: tuple[TunableParameter, ...] = ()
|
||||
assurance_claims: tuple[AssuranceClaim, ...] = ()
|
||||
eligibility: tuple[str, ...] = ()
|
||||
provider_hints: dict[str, Any] = field(default_factory=dict)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
|
@ -152,6 +171,36 @@ def _parse_tunable_parameter(raw: dict[str, Any]) -> TunableParameter:
|
|||
)
|
||||
|
||||
|
||||
def _parse_assurance_claim(raw: dict[str, Any]) -> AssuranceClaim:
|
||||
def integer_or_raw(value: Any) -> Any:
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return value
|
||||
return value
|
||||
|
||||
return AssuranceClaim(
|
||||
id=str(raw.get("id", "")),
|
||||
kind=str(raw.get("kind", "")),
|
||||
customer_wording=str(raw.get("customer_wording", "")),
|
||||
minimum_levels={
|
||||
str(axis): integer_or_raw(level)
|
||||
for axis, level in raw.get("minimum_levels", {}).items()
|
||||
},
|
||||
delivering_service=str(raw.get("delivering_service", "")),
|
||||
evidence_ref=str(raw.get("evidence_ref", "")),
|
||||
maximum_erasure_horizon_days=(
|
||||
integer_or_raw(raw["maximum_erasure_horizon_days"])
|
||||
if raw.get("maximum_erasure_horizon_days") is not None
|
||||
else None
|
||||
),
|
||||
provider_contract_ref=raw.get("provider_contract_ref"),
|
||||
resource_governor_ref=raw.get("resource_governor_ref"),
|
||||
erasure_mechanism=raw.get("erasure_mechanism"),
|
||||
)
|
||||
|
||||
|
||||
def _legacy_charge_components(raw: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
components: list[dict[str, Any]] = [
|
||||
{
|
||||
|
|
@ -233,6 +282,9 @@ def _parse_pricing_model(raw: dict[str, Any]) -> PricingModel:
|
|||
tunable_parameters=tuple(
|
||||
_parse_tunable_parameter(item) for item in raw.get("tunable_parameters", [])
|
||||
),
|
||||
assurance_claims=tuple(
|
||||
_parse_assurance_claim(item) for item in raw.get("assurance_claims", [])
|
||||
),
|
||||
eligibility=tuple(str(item) for item in raw.get("eligibility", [])),
|
||||
provider_hints=_tuple_dict(raw.get("provider_hints")),
|
||||
metadata=metadata,
|
||||
|
|
@ -320,4 +372,46 @@ def validate_pricing_model(model: PricingModel) -> list[str]:
|
|||
if len(commitment_ids) != len(set(commitment_ids)):
|
||||
issues.append("commitment ids must be unique")
|
||||
|
||||
claim_ids = [claim.id for claim in model.assurance_claims]
|
||||
if len(claim_ids) != len(set(claim_ids)):
|
||||
issues.append("assurance claim ids must be unique")
|
||||
for claim in model.assurance_claims:
|
||||
prefix = f"assurance claim '{claim.id or '<missing>'}'"
|
||||
if not claim.id.strip():
|
||||
issues.append("assurance claim id is required")
|
||||
if claim.kind not in _ALLOWED_ASSURANCE_KINDS:
|
||||
issues.append(f"{prefix} has unsupported kind '{claim.kind}'")
|
||||
for field_name, value in (
|
||||
("customer_wording", claim.customer_wording),
|
||||
("delivering_service", claim.delivering_service),
|
||||
("evidence_ref", claim.evidence_ref),
|
||||
):
|
||||
if not value.strip():
|
||||
issues.append(f"{prefix} requires {field_name}")
|
||||
if not claim.minimum_levels:
|
||||
issues.append(f"{prefix} requires minimum_levels")
|
||||
for axis, level in claim.minimum_levels.items():
|
||||
if axis not in _AXIS_MAXIMUMS:
|
||||
issues.append(f"{prefix} has unknown axis '{axis}'")
|
||||
elif (
|
||||
isinstance(level, bool)
|
||||
or not isinstance(level, int)
|
||||
or not 0 <= level <= _AXIS_MAXIMUMS[axis]
|
||||
):
|
||||
issues.append(f"{prefix} has invalid {axis} level {level!r}")
|
||||
if (
|
||||
claim.maximum_erasure_horizon_days is not None
|
||||
and (
|
||||
isinstance(claim.maximum_erasure_horizon_days, bool)
|
||||
or not isinstance(claim.maximum_erasure_horizon_days, int)
|
||||
or claim.maximum_erasure_horizon_days < 1
|
||||
)
|
||||
):
|
||||
issues.append(f"{prefix} has an invalid maximum erasure horizon")
|
||||
if (
|
||||
claim.erasure_mechanism is not None
|
||||
and claim.erasure_mechanism not in _ALLOWED_ERASURE_MECHANISMS
|
||||
):
|
||||
issues.append(f"{prefix} has unsupported erasure_mechanism")
|
||||
|
||||
return issues
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue