Files
adaptive-pricing/adaptive_pricing_core/provider_publication.py

697 lines
24 KiB
Python

from __future__ import annotations
import json
from dataclasses import dataclass, field
from decimal import Decimal
from typing import Any, Literal
from .pricing_models import PricingModel
PublicationArtifactKind = Literal["product", "meter", "price", "commitment", "configuration"]
ArtifactMappingStatus = Literal["exact", "approximate", "unsupported"]
PublicationOperationKind = Literal["create", "update", "noop", "retire", "rollback"]
DriftSeverity = Literal["info", "warn", "error"]
def _serialize_value(value: Any) -> Any:
if isinstance(value, Decimal):
return str(value)
if hasattr(value, "__dataclass_fields__"):
return {
key: _serialize_value(getattr(value, key))
for key in value.__dataclass_fields__
}
if isinstance(value, tuple):
return [_serialize_value(item) for item in value]
if isinstance(value, list):
return [_serialize_value(item) for item in value]
if isinstance(value, dict):
return {key: _serialize_value(item) for key, item in value.items()}
return value
def _payload_signature(payload: Any) -> str:
return json.dumps(_serialize_value(payload), sort_keys=True)
@dataclass(frozen=True)
class CatalogProduct:
id: str
name: str
description: str
currency: str
lifecycle_phase: str
active_pricing_model_id: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableProduct:
key: str
product_id: str
name: str
description: str
currency: str
lifecycle_phase: str
active: bool
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableMeter:
key: str
meter_id: str
name: str
event_name: str
unit: str
aggregation: str = "sum"
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishablePrice:
key: str
price_id: str
product_key: str
component_id: str
component_kind: str
label: str
currency: str
billing_treatment: str
cadence: str | None = None
amount: Decimal | None = None
unit_price: Decimal | None = None
included_units: Decimal | None = None
meter_key: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableCommitment:
key: str
commitment_id: str
kind: str
value: str
unit: str | None = None
description: str = ""
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublishableConfiguration:
key: str
configuration_id: str
product_key: str
model_id: str
model_name: str
segment: str | None
price_keys: tuple[str, ...]
commitment_keys: tuple[str, ...]
metadata: dict[str, Any] = field(default_factory=dict)
@dataclass(frozen=True)
class PublicationBundle:
bundle_id: str
model_id: str
model_name: str
product: PublishableProduct
meters: tuple[PublishableMeter, ...]
prices: tuple[PublishablePrice, ...]
commitments: tuple[PublishableCommitment, ...]
configurations: tuple[PublishableConfiguration, ...]
provider_hints: dict[str, Any] = field(default_factory=dict)
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class ProviderMappedArtifact:
provider: str
source_key: str
source_kind: PublicationArtifactKind
provider_id: str
provider_object_type: str
mapping_status: ArtifactMappingStatus
payload: dict[str, Any]
metadata: dict[str, Any] = field(default_factory=dict)
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class ProviderPublicationPackage:
provider: str
bundle_id: str
model_id: str
model_name: str
artifacts: tuple[ProviderMappedArtifact, ...]
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class DriftFinding:
provider_id: str
provider_object_type: str
severity: DriftSeverity
summary: str
expected: dict[str, Any] = field(default_factory=dict)
actual: dict[str, Any] = field(default_factory=dict)
suggested_action: str | None = None
@dataclass(frozen=True)
class PublicationOperation:
kind: PublicationOperationKind
provider_id: str
provider_object_type: str
source_key: str | None
source_kind: PublicationArtifactKind | None
mapping_status: ArtifactMappingStatus | None
summary: str
desired_payload: dict[str, Any] = field(default_factory=dict)
current_payload: dict[str, Any] = field(default_factory=dict)
desired_metadata: dict[str, Any] = field(default_factory=dict)
current_metadata: dict[str, Any] = field(default_factory=dict)
desired_notes: tuple[str, ...] = ()
current_notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class PublishedProviderArtifact:
provider: str
source_key: str
source_kind: PublicationArtifactKind
provider_id: str
provider_object_type: str
mapping_status: ArtifactMappingStatus
payload: dict[str, Any]
metadata: dict[str, Any] = field(default_factory=dict)
notes: tuple[str, ...] = ()
@dataclass(frozen=True)
class PublicationRevision:
revision_id: str
model_id: str
model_name: str
summary: str
operations: tuple[PublicationOperation, ...]
snapshot: tuple[PublishedProviderArtifact, ...]
replaced_revision_id: str | None = None
@dataclass(frozen=True)
class ProviderPublicationState:
provider: str
active_revision_id: str | None = None
active_model_id: str | None = None
artifacts: tuple[PublishedProviderArtifact, ...] = ()
revisions: tuple[PublicationRevision, ...] = ()
@dataclass(frozen=True)
class PublicationPlan:
provider: str
bundle_id: str
model_id: str
model_name: str
operations: tuple[PublicationOperation, ...]
drift: tuple[DriftFinding, ...]
unsupported_artifacts: tuple[ProviderMappedArtifact, ...]
summary: str
@dataclass(frozen=True)
class PublicationApplyResult:
plan: PublicationPlan
revision: PublicationRevision
state: ProviderPublicationState
summary: str
def build_publication_bundle(
product: CatalogProduct,
model: PricingModel,
*,
configuration_id: str | None = None,
segment: str | None = None,
) -> PublicationBundle:
product_key = f"product:{product.id}"
publishable_product = PublishableProduct(
key=product_key,
product_id=product.id,
name=product.name,
description=product.description,
currency=product.currency,
lifecycle_phase=product.lifecycle_phase,
active=model.status != "retired",
metadata={
**product.metadata,
"adaptive_pricing_model_id": model.id,
"adaptive_pricing_model_status": model.status,
},
)
meters: list[PublishableMeter] = []
prices: list[PublishablePrice] = []
for component in model.charge_components:
meter_key: str | None = None
if component.kind == "usage" and component.meter:
meter_key = f"meter:{model.id}:{component.id}"
meters.append(
PublishableMeter(
key=meter_key,
meter_id=component.meter,
name=component.label or component.meter,
event_name=component.meter,
unit=component.unit or "usage_unit",
metadata={
"adaptive_pricing_model_id": model.id,
"source_component_id": component.id,
},
)
)
prices.append(
PublishablePrice(
key=f"price:{model.id}:{component.id}",
price_id=f"{model.id}:{component.id}",
product_key=product_key,
component_id=component.id,
component_kind=component.kind,
label=component.label or component.id,
currency=model.currency,
billing_treatment=component.billing_treatment or "recurring",
cadence=component.cadence or (
model.access_fee_cadence if component.kind in {"access", "support", "discount", "risk_adjustment"} else None
),
amount=component.amount,
unit_price=component.unit_price,
included_units=component.included_units,
meter_key=meter_key,
metadata={
**component.metadata,
"adaptive_pricing_model_id": model.id,
"source_component_id": component.id,
"source_component_kind": component.kind,
},
)
)
commitments = tuple(
PublishableCommitment(
key=f"commitment:{model.id}:{commitment.id}",
commitment_id=commitment.id,
kind=commitment.kind,
value=commitment.value,
unit=commitment.unit,
description=commitment.description,
metadata={"adaptive_pricing_model_id": model.id},
)
for commitment in model.commitments
)
configuration = PublishableConfiguration(
key=f"configuration:{configuration_id or model.id}",
configuration_id=configuration_id or model.id,
product_key=product_key,
model_id=model.id,
model_name=model.name,
segment=segment,
price_keys=tuple(price.key for price in prices),
commitment_keys=tuple(commitment.key for commitment in commitments),
metadata={
"adaptive_pricing_model_id": model.id,
"lifecycle_phase": model.lifecycle_phase,
},
)
notes = (
"Publication bundles preserve the internal pricing model as the source of truth.",
"Provider mappings may mark artifacts exact, approximate, or unsupported without mutating the bundle.",
)
return PublicationBundle(
bundle_id=f"bundle:{model.id}",
model_id=model.id,
model_name=model.name,
product=publishable_product,
meters=tuple(meters),
prices=tuple(prices),
commitments=commitments,
configurations=(configuration,),
provider_hints=model.provider_hints,
notes=notes,
)
def _published_artifact(mapped: ProviderMappedArtifact) -> PublishedProviderArtifact:
return PublishedProviderArtifact(
provider=mapped.provider,
source_key=mapped.source_key,
source_kind=mapped.source_kind,
provider_id=mapped.provider_id,
provider_object_type=mapped.provider_object_type,
mapping_status=mapped.mapping_status,
payload=mapped.payload,
metadata=mapped.metadata,
notes=mapped.notes,
)
def _artifact_changed(
desired: ProviderMappedArtifact,
current: PublishedProviderArtifact,
) -> bool:
return any(
[
desired.mapping_status != current.mapping_status,
desired.provider_object_type != current.provider_object_type,
_payload_signature(desired.payload) != _payload_signature(current.payload),
_payload_signature(desired.metadata) != _payload_signature(current.metadata),
desired.notes != current.notes,
]
)
def _diff_summary(
desired: ProviderMappedArtifact,
current: PublishedProviderArtifact,
) -> DriftFinding:
return DriftFinding(
provider_id=desired.provider_id,
provider_object_type=desired.provider_object_type,
severity="warn",
summary="Provider shadow state differs from desired pricing definition.",
expected={
"payload": desired.payload,
"metadata": desired.metadata,
"mapping_status": desired.mapping_status,
"notes": list(desired.notes),
},
actual={
"payload": current.payload,
"metadata": current.metadata,
"mapping_status": current.mapping_status,
"notes": list(current.notes),
},
suggested_action="Publish the desired definition again or reconcile the provider-side drift.",
)
def plan_publication(
package: ProviderPublicationPackage,
current_state: ProviderPublicationState | None = None,
) -> PublicationPlan:
current_state = current_state or ProviderPublicationState(provider=package.provider)
current_index = {artifact.provider_id: artifact for artifact in current_state.artifacts}
desired_publishable = tuple(
artifact for artifact in package.artifacts if artifact.mapping_status != "unsupported"
)
unsupported = tuple(
artifact for artifact in package.artifacts if artifact.mapping_status == "unsupported"
)
operations: list[PublicationOperation] = []
drift: list[DriftFinding] = []
desired_ids = {artifact.provider_id for artifact in desired_publishable}
for artifact in desired_publishable:
current = current_index.get(artifact.provider_id)
if current is None:
operations.append(
PublicationOperation(
kind="create",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Create provider artifact from canonical pricing definition.",
desired_payload=artifact.payload,
desired_metadata=artifact.metadata,
desired_notes=artifact.notes,
)
)
continue
if _artifact_changed(artifact, current):
operations.append(
PublicationOperation(
kind="update",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Update provider artifact to match canonical pricing definition.",
desired_payload=artifact.payload,
current_payload=current.payload,
desired_metadata=artifact.metadata,
current_metadata=current.metadata,
desired_notes=artifact.notes,
current_notes=current.notes,
)
)
drift.append(_diff_summary(artifact, current))
continue
operations.append(
PublicationOperation(
kind="noop",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Provider artifact already matches canonical pricing definition.",
desired_payload=artifact.payload,
current_payload=current.payload,
desired_metadata=artifact.metadata,
current_metadata=current.metadata,
desired_notes=artifact.notes,
current_notes=current.notes,
)
)
for artifact in current_state.artifacts:
if artifact.provider_id in desired_ids:
continue
operations.append(
PublicationOperation(
kind="retire",
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
source_key=artifact.source_key,
source_kind=artifact.source_kind,
mapping_status=artifact.mapping_status,
summary="Retire managed provider artifact no longer present in the desired pricing definition.",
current_payload=artifact.payload,
current_metadata=artifact.metadata,
current_notes=artifact.notes,
)
)
drift.append(
DriftFinding(
provider_id=artifact.provider_id,
provider_object_type=artifact.provider_object_type,
severity="warn",
summary="Managed provider artifact exists in shadow state but not in the desired pricing definition.",
actual={
"payload": artifact.payload,
"metadata": artifact.metadata,
"mapping_status": artifact.mapping_status,
"notes": list(artifact.notes),
},
suggested_action="Retire the artifact or republish the desired model.",
)
)
summary = (
f"{package.provider}: {sum(op.kind == 'create' for op in operations)} create, "
f"{sum(op.kind == 'update' for op in operations)} update, "
f"{sum(op.kind == 'retire' for op in operations)} retire, "
f"{sum(op.kind == 'noop' for op in operations)} noop, "
f"{len(unsupported)} unsupported."
)
return PublicationPlan(
provider=package.provider,
bundle_id=package.bundle_id,
model_id=package.model_id,
model_name=package.model_name,
operations=tuple(operations),
drift=tuple(drift),
unsupported_artifacts=unsupported,
summary=summary,
)
def _next_revision_id(state: ProviderPublicationState) -> str:
return f"{state.provider}-rev-{len(state.revisions) + 1:04d}"
def _next_active_model_id(artifacts: list[PublishedProviderArtifact]) -> str | None:
product = next((artifact for artifact in artifacts if artifact.source_kind == "product"), None)
if product is None:
return None
if product.metadata.get("model_id"):
return str(product.metadata["model_id"])
payload_metadata = product.payload.get("metadata", {})
if payload_metadata.get("adaptive_pricing_model_id"):
return str(payload_metadata["adaptive_pricing_model_id"])
return None
def apply_publication(
package: ProviderPublicationPackage,
current_state: ProviderPublicationState | None = None,
) -> PublicationApplyResult:
current_state = current_state or ProviderPublicationState(provider=package.provider)
plan = plan_publication(package, current_state)
desired_index = {
artifact.provider_id: artifact
for artifact in package.artifacts
if artifact.mapping_status != "unsupported"
}
artifact_index = {artifact.provider_id: artifact for artifact in current_state.artifacts}
for operation in plan.operations:
if operation.kind in {"create", "update", "noop"}:
artifact_index[operation.provider_id] = _published_artifact(
desired_index[operation.provider_id]
)
elif operation.kind == "retire":
artifact_index.pop(operation.provider_id, None)
snapshot = tuple(sorted(artifact_index.values(), key=lambda artifact: artifact.provider_id))
revision = PublicationRevision(
revision_id=_next_revision_id(current_state),
model_id=package.model_id,
model_name=package.model_name,
summary=plan.summary,
operations=plan.operations,
snapshot=snapshot,
replaced_revision_id=current_state.active_revision_id,
)
state = ProviderPublicationState(
provider=package.provider,
active_revision_id=revision.revision_id,
active_model_id=_next_active_model_id(list(snapshot)),
artifacts=snapshot,
revisions=current_state.revisions + (revision,),
)
return PublicationApplyResult(
plan=plan,
revision=revision,
state=state,
summary=plan.summary,
)
def rollback_publication(
state: ProviderPublicationState,
revision_id: str,
) -> PublicationApplyResult:
revision = next((item for item in state.revisions if item.revision_id == revision_id), None)
if revision is None:
raise ValueError(f"unknown revision_id '{revision_id}'")
rollback_operation = PublicationOperation(
kind="rollback",
provider_id=revision.revision_id,
provider_object_type="revision",
source_key=None,
source_kind=None,
mapping_status=None,
summary=f"Rollback provider shadow state to {revision.revision_id}.",
)
new_revision = PublicationRevision(
revision_id=_next_revision_id(state),
model_id=revision.model_id,
model_name=revision.model_name,
summary=f"Rolled back provider shadow state to {revision.revision_id}.",
operations=(rollback_operation,),
snapshot=revision.snapshot,
replaced_revision_id=state.active_revision_id,
)
new_state = ProviderPublicationState(
provider=state.provider,
active_revision_id=new_revision.revision_id,
active_model_id=revision.model_id,
artifacts=revision.snapshot,
revisions=state.revisions + (new_revision,),
)
return PublicationApplyResult(
plan=PublicationPlan(
provider=state.provider,
bundle_id=f"rollback:{revision.revision_id}",
model_id=revision.model_id,
model_name=revision.model_name,
operations=(rollback_operation,),
drift=(),
unsupported_artifacts=(),
summary=new_revision.summary,
),
revision=new_revision,
state=new_state,
summary=new_revision.summary,
)
def provider_state_to_dict(state: ProviderPublicationState) -> dict[str, Any]:
return _serialize_value(state)
def _operation_from_dict(raw: dict[str, Any]) -> PublicationOperation:
return PublicationOperation(
kind=raw["kind"],
provider_id=raw["provider_id"],
provider_object_type=raw["provider_object_type"],
source_key=raw.get("source_key"),
source_kind=raw.get("source_kind"),
mapping_status=raw.get("mapping_status"),
summary=raw["summary"],
desired_payload=dict(raw.get("desired_payload", {})),
current_payload=dict(raw.get("current_payload", {})),
desired_metadata=dict(raw.get("desired_metadata", {})),
current_metadata=dict(raw.get("current_metadata", {})),
desired_notes=tuple(raw.get("desired_notes", [])),
current_notes=tuple(raw.get("current_notes", [])),
)
def _artifact_from_dict(raw: dict[str, Any]) -> PublishedProviderArtifact:
return PublishedProviderArtifact(
provider=raw["provider"],
source_key=raw["source_key"],
source_kind=raw["source_kind"],
provider_id=raw["provider_id"],
provider_object_type=raw["provider_object_type"],
mapping_status=raw["mapping_status"],
payload=dict(raw.get("payload", {})),
metadata=dict(raw.get("metadata", {})),
notes=tuple(raw.get("notes", [])),
)
def _revision_from_dict(raw: dict[str, Any]) -> PublicationRevision:
return PublicationRevision(
revision_id=raw["revision_id"],
model_id=raw["model_id"],
model_name=raw["model_name"],
summary=raw["summary"],
operations=tuple(_operation_from_dict(item) for item in raw.get("operations", [])),
snapshot=tuple(_artifact_from_dict(item) for item in raw.get("snapshot", [])),
replaced_revision_id=raw.get("replaced_revision_id"),
)
def provider_state_from_dict(raw: dict[str, Any]) -> ProviderPublicationState:
return ProviderPublicationState(
provider=raw["provider"],
active_revision_id=raw.get("active_revision_id"),
active_model_id=raw.get("active_model_id"),
artifacts=tuple(_artifact_from_dict(item) for item in raw.get("artifacts", [])),
revisions=tuple(_revision_from_dict(item) for item in raw.get("revisions", [])),
)