generated from coulomb/repo-seed
Implement financial Fabric vNext read model
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy import Boolean, DateTime, Float, ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -26,8 +26,15 @@ class FabricGraphImport(Base, TimestampMixin):
|
||||
source_commit: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True)
|
||||
source_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
api_version: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
schema_version: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
|
||||
export_kind: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
|
||||
exported_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
||||
netkingdom_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
king_actor_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
actor_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
||||
fabric_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
||||
unresolved_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
||||
compatibility: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict, server_default="{}")
|
||||
content_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
|
||||
node_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
||||
edge_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, server_default="0")
|
||||
@@ -72,6 +79,18 @@ class FabricGraphNode(Base):
|
||||
canon_anchor: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
mapping_fit: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
evidence_state: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
evidence_review_state: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
evidence_confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
netkingdom_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
fabric_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
subfabric_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
environment: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
|
||||
deployment_scenario_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
owner_actor_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
owner_role: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
ownership_resolution: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
cost_center_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
profit_center_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
display_only: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false", index=True)
|
||||
attributes: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict, server_default="{}")
|
||||
raw_json: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict, server_default="{}")
|
||||
@@ -103,6 +122,26 @@ class FabricGraphEdge(Base):
|
||||
canon_anchor: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
mapping_fit: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
evidence_state: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
evidence_review_state: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
evidence_confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
relationship_category: Mapped[str | None] = mapped_column(String(30), nullable=True, index=True)
|
||||
provider_owner_actor_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
provider_fabric_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
provider_subfabric_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
consumer_owner_actor_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
consumer_fabric_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
consumer_subfabric_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
crosses_fabric_boundary: Mapped[bool | None] = mapped_column(Boolean, nullable=True, index=True)
|
||||
crosses_subfabric_boundary: Mapped[bool | None] = mapped_column(Boolean, nullable=True, index=True)
|
||||
utility_type: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
|
||||
utility_contract_id: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
utility_payment_schema_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
utility_metering_basis: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
utility_business_model: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
|
||||
cost_center_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
profit_center_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
provider_profit_center_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
consumer_cost_center_id: Mapped[str | None] = mapped_column(Text, nullable=True, index=True)
|
||||
display_only: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false", index=True)
|
||||
attributes: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict, server_default="{}")
|
||||
raw_json: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict, server_default="{}")
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.database import get_session
|
||||
@@ -137,9 +137,18 @@ async def list_graph_nodes(
|
||||
domain: str | None = None,
|
||||
repo: str | None = None,
|
||||
canonical_category: str | None = None,
|
||||
fabric_id: str | None = None,
|
||||
subfabric_id: str | None = None,
|
||||
owner_actor_id: str | None = None,
|
||||
owner_role: str | None = None,
|
||||
ownership_resolution: str | None = None,
|
||||
cost_center_id: str | None = None,
|
||||
profit_center_id: str | None = None,
|
||||
evidence_state: str | None = None,
|
||||
evidence_review_state: str | None = None,
|
||||
mapping_fit: str | None = None,
|
||||
kind: str | None = None,
|
||||
unresolved_ownership: bool | None = None,
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
session: AsyncSession = Depends(get_session),
|
||||
) -> list[FabricGraphNodeRead]:
|
||||
@@ -151,12 +160,40 @@ async def list_graph_nodes(
|
||||
query = query.where(FabricGraphNode.repo_slug == repo)
|
||||
if canonical_category:
|
||||
query = query.where(FabricGraphNode.canon_category == canonical_category)
|
||||
if fabric_id:
|
||||
query = query.where(FabricGraphNode.fabric_id == fabric_id)
|
||||
if subfabric_id:
|
||||
query = query.where(FabricGraphNode.subfabric_id == subfabric_id)
|
||||
if owner_actor_id:
|
||||
query = query.where(FabricGraphNode.owner_actor_id == owner_actor_id)
|
||||
if owner_role:
|
||||
query = query.where(FabricGraphNode.owner_role == owner_role)
|
||||
if ownership_resolution:
|
||||
query = query.where(FabricGraphNode.ownership_resolution == ownership_resolution)
|
||||
if cost_center_id:
|
||||
query = query.where(FabricGraphNode.cost_center_id == cost_center_id)
|
||||
if profit_center_id:
|
||||
query = query.where(FabricGraphNode.profit_center_id == profit_center_id)
|
||||
if evidence_state:
|
||||
query = query.where(FabricGraphNode.evidence_state == evidence_state)
|
||||
if evidence_review_state:
|
||||
query = query.where(FabricGraphNode.evidence_review_state == evidence_review_state)
|
||||
if mapping_fit:
|
||||
query = query.where(FabricGraphNode.mapping_fit == mapping_fit)
|
||||
if kind:
|
||||
query = query.where(FabricGraphNode.kind == kind)
|
||||
if unresolved_ownership is True:
|
||||
query = query.where(
|
||||
or_(
|
||||
FabricGraphNode.owner_actor_id.is_(None),
|
||||
FabricGraphNode.ownership_resolution.in_(("unresolved", "ambiguous")),
|
||||
)
|
||||
)
|
||||
elif unresolved_ownership is False:
|
||||
query = query.where(
|
||||
FabricGraphNode.owner_actor_id.is_not(None),
|
||||
~FabricGraphNode.ownership_resolution.in_(("unresolved", "ambiguous")),
|
||||
)
|
||||
query = query.order_by(FabricGraphNode.graph_id).limit(limit)
|
||||
result = await session.execute(query)
|
||||
return [FabricGraphNodeRead.model_validate(row) for row in result.scalars().all()]
|
||||
@@ -167,9 +204,27 @@ async def list_graph_edges(
|
||||
source_repo_slug: str = "railiance-fabric",
|
||||
canonical_relationship: str | None = None,
|
||||
edge_type: str | None = None,
|
||||
relationship_category: str | None = None,
|
||||
provider_owner_actor_id: str | None = None,
|
||||
consumer_owner_actor_id: str | None = None,
|
||||
provider_fabric_id: str | None = None,
|
||||
consumer_fabric_id: str | None = None,
|
||||
provider_subfabric_id: str | None = None,
|
||||
consumer_subfabric_id: str | None = None,
|
||||
crosses_fabric_boundary: bool | None = None,
|
||||
crosses_subfabric_boundary: bool | None = None,
|
||||
utility_type: str | None = None,
|
||||
utility_business_model: str | None = None,
|
||||
utility_payment_schema_id: str | None = None,
|
||||
cost_center_id: str | None = None,
|
||||
profit_center_id: str | None = None,
|
||||
provider_profit_center_id: str | None = None,
|
||||
consumer_cost_center_id: str | None = None,
|
||||
evidence_state: str | None = None,
|
||||
evidence_review_state: str | None = None,
|
||||
mapping_fit: str | None = None,
|
||||
display_only: bool | None = None,
|
||||
missing_payment_schema: bool | None = None,
|
||||
from_graph_id: str | None = None,
|
||||
to_graph_id: str | None = None,
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
@@ -181,12 +236,58 @@ async def list_graph_edges(
|
||||
query = query.where(FabricGraphEdge.canonical_type == canonical_relationship)
|
||||
if edge_type:
|
||||
query = query.where(FabricGraphEdge.edge_type == edge_type)
|
||||
if relationship_category:
|
||||
query = query.where(FabricGraphEdge.relationship_category == relationship_category)
|
||||
if provider_owner_actor_id:
|
||||
query = query.where(FabricGraphEdge.provider_owner_actor_id == provider_owner_actor_id)
|
||||
if consumer_owner_actor_id:
|
||||
query = query.where(FabricGraphEdge.consumer_owner_actor_id == consumer_owner_actor_id)
|
||||
if provider_fabric_id:
|
||||
query = query.where(FabricGraphEdge.provider_fabric_id == provider_fabric_id)
|
||||
if consumer_fabric_id:
|
||||
query = query.where(FabricGraphEdge.consumer_fabric_id == consumer_fabric_id)
|
||||
if provider_subfabric_id:
|
||||
query = query.where(FabricGraphEdge.provider_subfabric_id == provider_subfabric_id)
|
||||
if consumer_subfabric_id:
|
||||
query = query.where(FabricGraphEdge.consumer_subfabric_id == consumer_subfabric_id)
|
||||
if crosses_fabric_boundary is not None:
|
||||
query = query.where(FabricGraphEdge.crosses_fabric_boundary == crosses_fabric_boundary)
|
||||
if crosses_subfabric_boundary is not None:
|
||||
query = query.where(FabricGraphEdge.crosses_subfabric_boundary == crosses_subfabric_boundary)
|
||||
if utility_type:
|
||||
query = query.where(FabricGraphEdge.utility_type == utility_type)
|
||||
if utility_business_model:
|
||||
query = query.where(FabricGraphEdge.utility_business_model == utility_business_model)
|
||||
if utility_payment_schema_id:
|
||||
query = query.where(FabricGraphEdge.utility_payment_schema_id == utility_payment_schema_id)
|
||||
if cost_center_id:
|
||||
query = query.where(FabricGraphEdge.cost_center_id == cost_center_id)
|
||||
if profit_center_id:
|
||||
query = query.where(FabricGraphEdge.profit_center_id == profit_center_id)
|
||||
if provider_profit_center_id:
|
||||
query = query.where(FabricGraphEdge.provider_profit_center_id == provider_profit_center_id)
|
||||
if consumer_cost_center_id:
|
||||
query = query.where(FabricGraphEdge.consumer_cost_center_id == consumer_cost_center_id)
|
||||
if evidence_state:
|
||||
query = query.where(FabricGraphEdge.evidence_state == evidence_state)
|
||||
if evidence_review_state:
|
||||
query = query.where(FabricGraphEdge.evidence_review_state == evidence_review_state)
|
||||
if mapping_fit:
|
||||
query = query.where(FabricGraphEdge.mapping_fit == mapping_fit)
|
||||
if display_only is not None:
|
||||
query = query.where(FabricGraphEdge.display_only == display_only)
|
||||
if missing_payment_schema is True:
|
||||
query = query.where(
|
||||
FabricGraphEdge.relationship_category == "utility",
|
||||
FabricGraphEdge.utility_payment_schema_id.is_(None),
|
||||
)
|
||||
elif missing_payment_schema is False:
|
||||
query = query.where(
|
||||
or_(
|
||||
FabricGraphEdge.relationship_category != "utility",
|
||||
FabricGraphEdge.utility_payment_schema_id.is_not(None),
|
||||
)
|
||||
)
|
||||
if from_graph_id:
|
||||
query = query.where(FabricGraphEdge.from_graph_id == from_graph_id)
|
||||
if to_graph_id:
|
||||
@@ -208,14 +309,32 @@ async def graph_summary(
|
||||
latest_import=None,
|
||||
node_count=0,
|
||||
edge_count=0,
|
||||
schema_version=None,
|
||||
netkingdom_id=None,
|
||||
actor_count=0,
|
||||
fabric_count=0,
|
||||
unresolved_count=0,
|
||||
nodes_by_domain={},
|
||||
nodes_by_repo={},
|
||||
nodes_by_canon_category={},
|
||||
nodes_by_fabric={},
|
||||
nodes_by_subfabric={},
|
||||
nodes_by_owner_actor={},
|
||||
nodes_by_owner_role={},
|
||||
nodes_by_ownership_resolution={},
|
||||
edges_by_canonical_type={},
|
||||
edges_by_relationship_category={},
|
||||
utility_edges_by_provider_owner={},
|
||||
utility_edges_by_consumer_owner={},
|
||||
utility_edges_by_business_model={},
|
||||
nodes_by_evidence_state={},
|
||||
edges_by_evidence_state={},
|
||||
nodes_by_mapping_fit={},
|
||||
edges_by_mapping_fit={},
|
||||
tenant_utilities_without_payment_schema=0,
|
||||
nodes_without_accounting_attribution=0,
|
||||
unresolved_ownership_count=0,
|
||||
unresolved_accounting_count=0,
|
||||
example_nodes=[],
|
||||
example_edges=[],
|
||||
)
|
||||
@@ -225,14 +344,74 @@ async def graph_summary(
|
||||
latest_import=FabricGraphImportRead.model_validate(import_run),
|
||||
node_count=import_run.node_count,
|
||||
edge_count=import_run.edge_count,
|
||||
schema_version=import_run.schema_version,
|
||||
netkingdom_id=import_run.netkingdom_id,
|
||||
actor_count=import_run.actor_count,
|
||||
fabric_count=import_run.fabric_count,
|
||||
unresolved_count=import_run.unresolved_count,
|
||||
nodes_by_domain=await _counts(session, FabricGraphNode.domain_slug, import_run.id),
|
||||
nodes_by_repo=await _counts(session, FabricGraphNode.repo_slug, import_run.id),
|
||||
nodes_by_canon_category=await _counts(session, FabricGraphNode.canon_category, import_run.id),
|
||||
nodes_by_fabric=await _counts(session, FabricGraphNode.fabric_id, import_run.id),
|
||||
nodes_by_subfabric=await _counts(session, FabricGraphNode.subfabric_id, import_run.id),
|
||||
nodes_by_owner_actor=await _counts(session, FabricGraphNode.owner_actor_id, import_run.id),
|
||||
nodes_by_owner_role=await _counts(session, FabricGraphNode.owner_role, import_run.id),
|
||||
nodes_by_ownership_resolution=await _counts(session, FabricGraphNode.ownership_resolution, import_run.id),
|
||||
edges_by_canonical_type=await _counts(session, FabricGraphEdge.canonical_type, import_run.id),
|
||||
edges_by_relationship_category=await _counts(session, FabricGraphEdge.relationship_category, import_run.id),
|
||||
utility_edges_by_provider_owner=await _counts(
|
||||
session,
|
||||
FabricGraphEdge.provider_owner_actor_id,
|
||||
import_run.id,
|
||||
FabricGraphEdge.relationship_category == "utility",
|
||||
),
|
||||
utility_edges_by_consumer_owner=await _counts(
|
||||
session,
|
||||
FabricGraphEdge.consumer_owner_actor_id,
|
||||
import_run.id,
|
||||
FabricGraphEdge.relationship_category == "utility",
|
||||
),
|
||||
utility_edges_by_business_model=await _counts(
|
||||
session,
|
||||
FabricGraphEdge.utility_business_model,
|
||||
import_run.id,
|
||||
FabricGraphEdge.relationship_category == "utility",
|
||||
),
|
||||
nodes_by_evidence_state=await _counts(session, FabricGraphNode.evidence_state, import_run.id),
|
||||
edges_by_evidence_state=await _counts(session, FabricGraphEdge.evidence_state, import_run.id),
|
||||
nodes_by_mapping_fit=await _counts(session, FabricGraphNode.mapping_fit, import_run.id),
|
||||
edges_by_mapping_fit=await _counts(session, FabricGraphEdge.mapping_fit, import_run.id),
|
||||
tenant_utilities_without_payment_schema=await _count_rows(
|
||||
session,
|
||||
FabricGraphEdge,
|
||||
import_run.id,
|
||||
FabricGraphEdge.relationship_category == "utility",
|
||||
FabricGraphEdge.utility_business_model == "tenant_utility",
|
||||
FabricGraphEdge.utility_payment_schema_id.is_(None),
|
||||
),
|
||||
nodes_without_accounting_attribution=await _count_rows(
|
||||
session,
|
||||
FabricGraphNode,
|
||||
import_run.id,
|
||||
FabricGraphNode.cost_center_id.is_(None),
|
||||
FabricGraphNode.profit_center_id.is_(None),
|
||||
),
|
||||
unresolved_ownership_count=await _count_rows(
|
||||
session,
|
||||
FabricGraphNode,
|
||||
import_run.id,
|
||||
or_(
|
||||
FabricGraphNode.owner_actor_id.is_(None),
|
||||
FabricGraphNode.ownership_resolution.in_(("unresolved", "ambiguous")),
|
||||
),
|
||||
),
|
||||
unresolved_accounting_count=await _count_rows(
|
||||
session,
|
||||
FabricGraphEdge,
|
||||
import_run.id,
|
||||
FabricGraphEdge.relationship_category == "utility",
|
||||
FabricGraphEdge.utility_payment_schema_id.is_(None),
|
||||
),
|
||||
example_nodes=await _example_nodes(session, import_run.id),
|
||||
example_edges=await _example_edges(session, import_run.id),
|
||||
)
|
||||
@@ -259,17 +438,24 @@ async def _latest_valid_import_or_404(session: AsyncSession, source_repo_slug: s
|
||||
return import_run
|
||||
|
||||
|
||||
async def _counts(session: AsyncSession, column: Any, import_id: Any) -> dict[str, int]:
|
||||
async def _counts(session: AsyncSession, column: Any, import_id: Any, *conditions: Any) -> dict[str, int]:
|
||||
table = column.class_
|
||||
result = await session.execute(
|
||||
select(column, func.count())
|
||||
.select_from(table)
|
||||
.where(table.import_id == import_id, column.is_not(None))
|
||||
.where(table.import_id == import_id, column.is_not(None), *conditions)
|
||||
.group_by(column)
|
||||
)
|
||||
return {str(key): int(count) for key, count in result.all() if key is not None}
|
||||
|
||||
|
||||
async def _count_rows(session: AsyncSession, table: Any, import_id: Any, *conditions: Any) -> int:
|
||||
result = await session.execute(
|
||||
select(func.count()).select_from(table).where(table.import_id == import_id, *conditions)
|
||||
)
|
||||
return int(result.scalar_one())
|
||||
|
||||
|
||||
async def _example_nodes(session: AsyncSession, import_id: Any) -> list[FabricGraphNodeRead]:
|
||||
result = await session.execute(
|
||||
select(FabricGraphNode)
|
||||
|
||||
@@ -7,14 +7,139 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
MappingFit = Literal["direct", "partial", "conflict", "gap", "unknown"]
|
||||
EvidenceState = Literal["observed", "declared", "inferred", "proposed", "gap"]
|
||||
FabricGraphApiVersion = Literal["railiance.fabric/v1alpha1", "railiance.fabric/v1alpha2"]
|
||||
ActorRole = Literal["king", "lord", "tenant", "operator", "steward"]
|
||||
OwnershipResolution = Literal["explicit", "inherited", "unresolved", "ambiguous"]
|
||||
EvidenceReviewState = Literal["accepted", "candidate", "needs_review", "rejected"]
|
||||
RelationshipCategory = Literal[
|
||||
"containment",
|
||||
"ownership",
|
||||
"technical",
|
||||
"utility",
|
||||
"accounting",
|
||||
"evidence",
|
||||
]
|
||||
|
||||
|
||||
class FabricGraphSource(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
repo: str | None = None
|
||||
producer: str | None = None
|
||||
registry: str | None = None
|
||||
commit: str | None = None
|
||||
path: str | None = None
|
||||
generation_reason: str | None = None
|
||||
|
||||
|
||||
class FabricGraphNetkingdomPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
id: str
|
||||
name: str
|
||||
king_actor_id: str
|
||||
|
||||
|
||||
class FabricGraphActorPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
id: str
|
||||
kind: Literal["FabricActor"]
|
||||
role: ActorRole
|
||||
name: str
|
||||
|
||||
|
||||
class FabricGraphFabricPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
id: str
|
||||
kind: Literal["Fabric", "Subfabric"]
|
||||
name: str
|
||||
netkingdom_id: str
|
||||
parent_fabric_id: str | None = None
|
||||
lord_actor_id: str | None = None
|
||||
tenant_actor_id: str | None = None
|
||||
status: str
|
||||
boundary: dict[str, Any] = Field(default_factory=dict)
|
||||
evidence_refs: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FabricGraphContainmentPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
netkingdom_id: str
|
||||
fabric_id: str
|
||||
subfabric_id: str | None = None
|
||||
environment: str | None = None
|
||||
deployment_scenario_id: str | None = None
|
||||
|
||||
|
||||
class FabricGraphOwnershipPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
owner_actor_id: str
|
||||
owner_role: ActorRole
|
||||
resolution: OwnershipResolution
|
||||
inherited_from: str | None = None
|
||||
supporting_actor_ids: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FabricGraphAccountingPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
cost_center_id: str | None = None
|
||||
profit_center_id: str | None = None
|
||||
provider_profit_center_id: str | None = None
|
||||
consumer_cost_center_id: str | None = None
|
||||
allocation_model: str | None = None
|
||||
payment_schema_id: str | None = None
|
||||
metering_basis: str | None = None
|
||||
valid_from: str | None = None
|
||||
valid_until: str | None = None
|
||||
|
||||
|
||||
class FabricGraphEvidencePayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
state: EvidenceState
|
||||
review_state: EvidenceReviewState
|
||||
confidence: float | None = Field(default=None, ge=0, le=1)
|
||||
refs: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FabricGraphUtilitySidePayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
owner_actor_id: str
|
||||
fabric_id: str
|
||||
subfabric_id: str | None = None
|
||||
|
||||
|
||||
class FabricGraphBoundaryPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
crosses_fabric_boundary: bool
|
||||
crosses_subfabric_boundary: bool
|
||||
|
||||
|
||||
class FabricGraphUtilityPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
utility_type: str
|
||||
contract_id: str | None = None
|
||||
payment_schema_id: str | None = None
|
||||
metering_basis: str | None = None
|
||||
business_model: str | None = None
|
||||
|
||||
|
||||
class FabricGraphUnresolvedPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
target_id: str
|
||||
kind: str
|
||||
severity: str | None = None
|
||||
message: str
|
||||
evidence_refs: list[dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FabricGraphNodePayload(BaseModel):
|
||||
@@ -23,9 +148,13 @@ class FabricGraphNodePayload(BaseModel):
|
||||
id: str
|
||||
kind: str
|
||||
name: str
|
||||
repo: str
|
||||
domain: str
|
||||
lifecycle: str
|
||||
repo: str | None = None
|
||||
domain: str | None = None
|
||||
lifecycle: str | None = None
|
||||
containment: FabricGraphContainmentPayload | None = None
|
||||
ownership: FabricGraphOwnershipPayload | None = None
|
||||
accounting: FabricGraphAccountingPayload | None = None
|
||||
evidence: FabricGraphEvidencePayload | None = None
|
||||
canon_category: str | None = None
|
||||
canon_anchor: str | None = None
|
||||
mapping_fit: MappingFit | None = None
|
||||
@@ -39,23 +168,37 @@ class FabricGraphEdgePayload(BaseModel):
|
||||
from_graph_id: str = Field(alias="from")
|
||||
to_graph_id: str = Field(alias="to")
|
||||
edge_type: str = Field(alias="type")
|
||||
id: str | None = None
|
||||
relationship_category: RelationshipCategory | None = None
|
||||
canonical_type: str | None = None
|
||||
canon_anchor: str | None = None
|
||||
mapping_fit: MappingFit | None = None
|
||||
display_only: bool | None = None
|
||||
evidence_state: EvidenceState | None = None
|
||||
provider: FabricGraphUtilitySidePayload | None = None
|
||||
consumer: FabricGraphUtilitySidePayload | None = None
|
||||
boundary: FabricGraphBoundaryPayload | None = None
|
||||
utility: FabricGraphUtilityPayload | None = None
|
||||
accounting: FabricGraphAccountingPayload | None = None
|
||||
evidence: FabricGraphEvidencePayload | None = None
|
||||
attributes: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class FabricGraphExportPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
||||
|
||||
api_version: Literal["railiance.fabric/v1alpha1"] = Field(alias="apiVersion")
|
||||
api_version: FabricGraphApiVersion = Field(alias="apiVersion")
|
||||
kind: Literal["FabricGraphExport"]
|
||||
schema_version: Literal["financial-fabric-v1"] | None = None
|
||||
generated_at: datetime | None = None
|
||||
source: FabricGraphSource | None = None
|
||||
compatibility: dict[str, Any] = Field(default_factory=dict)
|
||||
netkingdom: FabricGraphNetkingdomPayload | None = None
|
||||
actors: list[FabricGraphActorPayload] = Field(default_factory=list)
|
||||
fabrics: list[FabricGraphFabricPayload] = Field(default_factory=list)
|
||||
nodes: list[FabricGraphNodePayload]
|
||||
edges: list[FabricGraphEdgePayload]
|
||||
unresolved: list[FabricGraphUnresolvedPayload] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FabricGraphPullRequest(BaseModel):
|
||||
@@ -73,8 +216,15 @@ class FabricGraphImportRead(BaseModel):
|
||||
source_commit: str | None = None
|
||||
source_path: str | None = None
|
||||
api_version: str | None = None
|
||||
schema_version: str | None = None
|
||||
export_kind: str | None = None
|
||||
exported_at: datetime | None = None
|
||||
netkingdom_id: str | None = None
|
||||
king_actor_id: str | None = None
|
||||
actor_count: int
|
||||
fabric_count: int
|
||||
unresolved_count: int
|
||||
compatibility: dict
|
||||
content_hash: str
|
||||
node_count: int
|
||||
edge_count: int
|
||||
@@ -111,6 +261,18 @@ class FabricGraphNodeRead(BaseModel):
|
||||
canon_anchor: str | None = None
|
||||
mapping_fit: str | None = None
|
||||
evidence_state: str | None = None
|
||||
evidence_review_state: str | None = None
|
||||
evidence_confidence: float | None = None
|
||||
netkingdom_id: str | None = None
|
||||
fabric_id: str | None = None
|
||||
subfabric_id: str | None = None
|
||||
environment: str | None = None
|
||||
deployment_scenario_id: str | None = None
|
||||
owner_actor_id: str | None = None
|
||||
owner_role: str | None = None
|
||||
ownership_resolution: str | None = None
|
||||
cost_center_id: str | None = None
|
||||
profit_center_id: str | None = None
|
||||
display_only: bool
|
||||
attributes: dict
|
||||
raw_json: dict
|
||||
@@ -130,6 +292,26 @@ class FabricGraphEdgeRead(BaseModel):
|
||||
canon_anchor: str | None = None
|
||||
mapping_fit: str | None = None
|
||||
evidence_state: str | None = None
|
||||
evidence_review_state: str | None = None
|
||||
evidence_confidence: float | None = None
|
||||
relationship_category: str | None = None
|
||||
provider_owner_actor_id: str | None = None
|
||||
provider_fabric_id: str | None = None
|
||||
provider_subfabric_id: str | None = None
|
||||
consumer_owner_actor_id: str | None = None
|
||||
consumer_fabric_id: str | None = None
|
||||
consumer_subfabric_id: str | None = None
|
||||
crosses_fabric_boundary: bool | None = None
|
||||
crosses_subfabric_boundary: bool | None = None
|
||||
utility_type: str | None = None
|
||||
utility_contract_id: str | None = None
|
||||
utility_payment_schema_id: str | None = None
|
||||
utility_metering_basis: str | None = None
|
||||
utility_business_model: str | None = None
|
||||
cost_center_id: str | None = None
|
||||
profit_center_id: str | None = None
|
||||
provider_profit_center_id: str | None = None
|
||||
consumer_cost_center_id: str | None = None
|
||||
display_only: bool
|
||||
attributes: dict
|
||||
raw_json: dict
|
||||
@@ -140,13 +322,31 @@ class FabricGraphSummary(BaseModel):
|
||||
latest_import: FabricGraphImportRead | None = None
|
||||
node_count: int
|
||||
edge_count: int
|
||||
schema_version: str | None = None
|
||||
netkingdom_id: str | None = None
|
||||
actor_count: int
|
||||
fabric_count: int
|
||||
unresolved_count: int
|
||||
nodes_by_domain: dict[str, int]
|
||||
nodes_by_repo: dict[str, int]
|
||||
nodes_by_canon_category: dict[str, int]
|
||||
nodes_by_fabric: dict[str, int]
|
||||
nodes_by_subfabric: dict[str, int]
|
||||
nodes_by_owner_actor: dict[str, int]
|
||||
nodes_by_owner_role: dict[str, int]
|
||||
nodes_by_ownership_resolution: dict[str, int]
|
||||
edges_by_canonical_type: dict[str, int]
|
||||
edges_by_relationship_category: dict[str, int]
|
||||
utility_edges_by_provider_owner: dict[str, int]
|
||||
utility_edges_by_consumer_owner: dict[str, int]
|
||||
utility_edges_by_business_model: dict[str, int]
|
||||
nodes_by_evidence_state: dict[str, int]
|
||||
edges_by_evidence_state: dict[str, int]
|
||||
nodes_by_mapping_fit: dict[str, int]
|
||||
edges_by_mapping_fit: dict[str, int]
|
||||
tenant_utilities_without_payment_schema: int
|
||||
nodes_without_accounting_attribution: int
|
||||
unresolved_ownership_count: int
|
||||
unresolved_accounting_count: int
|
||||
example_nodes: list[FabricGraphNodeRead]
|
||||
example_edges: list[FabricGraphEdgeRead]
|
||||
|
||||
@@ -97,8 +97,15 @@ async def ingest_fabric_graph_export(
|
||||
source_commit=source.commit if source else None,
|
||||
source_path=source.path if source else None,
|
||||
api_version=export.api_version,
|
||||
schema_version=export.schema_version,
|
||||
export_kind=export.kind,
|
||||
exported_at=export.generated_at,
|
||||
netkingdom_id=export.netkingdom.id if export.netkingdom else None,
|
||||
king_actor_id=export.netkingdom.king_actor_id if export.netkingdom else None,
|
||||
actor_count=len(export.actors),
|
||||
fabric_count=len(export.fabrics),
|
||||
unresolved_count=len(export.unresolved),
|
||||
compatibility=export.compatibility,
|
||||
content_hash=content_hash,
|
||||
node_count=len(export.nodes),
|
||||
edge_count=len(export.edges),
|
||||
@@ -114,6 +121,10 @@ async def ingest_fabric_graph_export(
|
||||
|
||||
for node in export.nodes:
|
||||
raw = node.model_dump(mode="json")
|
||||
containment = raw.get("containment") if isinstance(raw.get("containment"), dict) else {}
|
||||
ownership = raw.get("ownership") if isinstance(raw.get("ownership"), dict) else {}
|
||||
accounting = raw.get("accounting") if isinstance(raw.get("accounting"), dict) else {}
|
||||
evidence = raw.get("evidence") if isinstance(raw.get("evidence"), dict) else {}
|
||||
session.add(
|
||||
FabricGraphNode(
|
||||
import_id=import_run.id,
|
||||
@@ -121,14 +132,26 @@ async def ingest_fabric_graph_export(
|
||||
graph_id=node.id,
|
||||
kind=node.kind,
|
||||
name=node.name,
|
||||
repo_slug=node.repo,
|
||||
domain_slug=node.domain,
|
||||
lifecycle=node.lifecycle,
|
||||
repo_slug=node.repo or "",
|
||||
domain_slug=node.domain or "",
|
||||
lifecycle=node.lifecycle or "",
|
||||
canonical_type=raw.get("canonical_type"),
|
||||
canon_category=node.canon_category,
|
||||
canon_anchor=node.canon_anchor,
|
||||
mapping_fit=node.mapping_fit,
|
||||
evidence_state=node.evidence_state,
|
||||
evidence_state=node.evidence_state or evidence.get("state"),
|
||||
evidence_review_state=evidence.get("review_state"),
|
||||
evidence_confidence=_float_or_none(evidence.get("confidence")),
|
||||
netkingdom_id=containment.get("netkingdom_id"),
|
||||
fabric_id=containment.get("fabric_id"),
|
||||
subfabric_id=containment.get("subfabric_id"),
|
||||
environment=containment.get("environment"),
|
||||
deployment_scenario_id=containment.get("deployment_scenario_id"),
|
||||
owner_actor_id=ownership.get("owner_actor_id"),
|
||||
owner_role=ownership.get("owner_role"),
|
||||
ownership_resolution=ownership.get("resolution"),
|
||||
cost_center_id=accounting.get("cost_center_id"),
|
||||
profit_center_id=accounting.get("profit_center_id"),
|
||||
display_only=bool(raw.get("display_only", False)),
|
||||
attributes=node.attributes,
|
||||
raw_json=raw,
|
||||
@@ -137,6 +160,12 @@ async def ingest_fabric_graph_export(
|
||||
|
||||
for edge in export.edges:
|
||||
raw = edge.model_dump(mode="json", by_alias=True)
|
||||
provider = raw.get("provider") if isinstance(raw.get("provider"), dict) else {}
|
||||
consumer = raw.get("consumer") if isinstance(raw.get("consumer"), dict) else {}
|
||||
boundary = raw.get("boundary") if isinstance(raw.get("boundary"), dict) else {}
|
||||
utility = raw.get("utility") if isinstance(raw.get("utility"), dict) else {}
|
||||
accounting = raw.get("accounting") if isinstance(raw.get("accounting"), dict) else {}
|
||||
evidence = raw.get("evidence") if isinstance(raw.get("evidence"), dict) else {}
|
||||
session.add(
|
||||
FabricGraphEdge(
|
||||
import_id=import_run.id,
|
||||
@@ -148,7 +177,27 @@ async def ingest_fabric_graph_export(
|
||||
canonical_type=edge.canonical_type,
|
||||
canon_anchor=edge.canon_anchor,
|
||||
mapping_fit=edge.mapping_fit,
|
||||
evidence_state=edge.evidence_state,
|
||||
evidence_state=edge.evidence_state or evidence.get("state"),
|
||||
evidence_review_state=evidence.get("review_state"),
|
||||
evidence_confidence=_float_or_none(evidence.get("confidence")),
|
||||
relationship_category=edge.relationship_category,
|
||||
provider_owner_actor_id=provider.get("owner_actor_id"),
|
||||
provider_fabric_id=provider.get("fabric_id"),
|
||||
provider_subfabric_id=provider.get("subfabric_id"),
|
||||
consumer_owner_actor_id=consumer.get("owner_actor_id"),
|
||||
consumer_fabric_id=consumer.get("fabric_id"),
|
||||
consumer_subfabric_id=consumer.get("subfabric_id"),
|
||||
crosses_fabric_boundary=boundary.get("crosses_fabric_boundary"),
|
||||
crosses_subfabric_boundary=boundary.get("crosses_subfabric_boundary"),
|
||||
utility_type=utility.get("utility_type"),
|
||||
utility_contract_id=utility.get("contract_id"),
|
||||
utility_payment_schema_id=utility.get("payment_schema_id"),
|
||||
utility_metering_basis=utility.get("metering_basis"),
|
||||
utility_business_model=utility.get("business_model"),
|
||||
cost_center_id=accounting.get("cost_center_id"),
|
||||
profit_center_id=accounting.get("profit_center_id"),
|
||||
provider_profit_center_id=accounting.get("provider_profit_center_id"),
|
||||
consumer_cost_center_id=accounting.get("consumer_cost_center_id"),
|
||||
display_only=bool(edge.display_only),
|
||||
attributes=edge.attributes,
|
||||
raw_json=raw,
|
||||
@@ -181,6 +230,9 @@ def validate_fabric_graph_export(payload: dict[str, Any]) -> FabricGraphExportPa
|
||||
message = first.get("msg", "invalid payload")
|
||||
raise ValueError(f"invalid FabricGraphExport at {location}: {message}") from exc
|
||||
|
||||
contract_errors = _contract_errors(export, payload)
|
||||
if contract_errors:
|
||||
raise ValueError(f"invalid FabricGraphExport contract: {contract_errors[0]}")
|
||||
canon_errors = _canon_metadata_errors(export)
|
||||
if canon_errors:
|
||||
raise ValueError(f"invalid FabricGraphExport canon metadata: {canon_errors[0]}")
|
||||
@@ -198,6 +250,10 @@ def edge_key(edge: dict[str, Any]) -> str:
|
||||
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _float_or_none(value: Any) -> float | None:
|
||||
return float(value) if isinstance(value, (int, float)) else None
|
||||
|
||||
|
||||
async def record_fabric_graph_error(
|
||||
session: AsyncSession,
|
||||
summary: str,
|
||||
@@ -318,6 +374,180 @@ def _canonical_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return canonical
|
||||
|
||||
|
||||
def _contract_errors(export: FabricGraphExportPayload, payload: dict[str, Any]) -> list[str]:
|
||||
if export.api_version == "railiance.fabric/v1alpha2":
|
||||
return _financial_contract_errors(export, payload)
|
||||
return _legacy_contract_errors(export)
|
||||
|
||||
|
||||
def _legacy_contract_errors(export: FabricGraphExportPayload) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if export.schema_version:
|
||||
errors.append("v1alpha1 exports must not set schema_version")
|
||||
for index, node in enumerate(export.nodes):
|
||||
_require_fields(
|
||||
errors,
|
||||
f"nodes[{index}]",
|
||||
{
|
||||
"repo": node.repo,
|
||||
"domain": node.domain,
|
||||
"lifecycle": node.lifecycle,
|
||||
},
|
||||
("repo", "domain", "lifecycle"),
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def _financial_contract_errors(
|
||||
export: FabricGraphExportPayload, payload: dict[str, Any]
|
||||
) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for field in ("schema_version", "netkingdom", "actors", "fabrics"):
|
||||
if field not in payload:
|
||||
errors.append(f"missing required financial export field {field!r}")
|
||||
if export.schema_version != "financial-fabric-v1":
|
||||
errors.append("schema_version must be 'financial-fabric-v1' for v1alpha2 exports")
|
||||
if export.netkingdom is None:
|
||||
errors.append("netkingdom must be an object for v1alpha2 exports")
|
||||
netkingdom_id = ""
|
||||
king_actor_id = ""
|
||||
else:
|
||||
netkingdom_id = export.netkingdom.id
|
||||
king_actor_id = export.netkingdom.king_actor_id
|
||||
|
||||
actor_roles: dict[str, str] = {}
|
||||
for index, actor in enumerate(export.actors):
|
||||
if actor.id in actor_roles:
|
||||
errors.append(f"actors[{index}].id {actor.id!r} is duplicated")
|
||||
actor_roles[actor.id] = actor.role
|
||||
if king_actor_id and actor_roles.get(king_actor_id) != "king":
|
||||
errors.append("netkingdom.king_actor_id must reference an actor with role 'king'")
|
||||
|
||||
fabric_kinds: dict[str, str] = {}
|
||||
for index, fabric in enumerate(export.fabrics):
|
||||
if fabric.id in fabric_kinds:
|
||||
errors.append(f"fabrics[{index}].id {fabric.id!r} is duplicated")
|
||||
fabric_kinds[fabric.id] = fabric.kind
|
||||
if fabric.netkingdom_id != netkingdom_id:
|
||||
errors.append(f"fabrics[{index}].netkingdom_id must match netkingdom.id")
|
||||
if fabric.kind == "Fabric":
|
||||
if not fabric.lord_actor_id:
|
||||
errors.append(f"fabrics[{index}].lord_actor_id is required for Fabric")
|
||||
elif actor_roles.get(fabric.lord_actor_id) not in {"lord", "king"}:
|
||||
errors.append(f"fabrics[{index}].lord_actor_id must reference a lord or king actor")
|
||||
if fabric.kind == "Subfabric":
|
||||
if not fabric.parent_fabric_id:
|
||||
errors.append(f"fabrics[{index}].parent_fabric_id is required for Subfabric")
|
||||
elif fabric.parent_fabric_id not in fabric_kinds:
|
||||
errors.append(
|
||||
f"fabrics[{index}].parent_fabric_id references unknown fabric {fabric.parent_fabric_id!r}"
|
||||
)
|
||||
if not fabric.tenant_actor_id:
|
||||
errors.append(f"fabrics[{index}].tenant_actor_id is required for Subfabric")
|
||||
elif actor_roles.get(fabric.tenant_actor_id) != "tenant":
|
||||
errors.append(f"fabrics[{index}].tenant_actor_id must reference a tenant actor")
|
||||
|
||||
node_ids: set[str] = set()
|
||||
for index, node in enumerate(export.nodes):
|
||||
path = f"nodes[{index}]"
|
||||
if node.id in node_ids:
|
||||
errors.append(f"{path}.id {node.id!r} is duplicated")
|
||||
node_ids.add(node.id)
|
||||
if node.containment is None:
|
||||
errors.append(f"{path}.containment must be an object for v1alpha2 exports")
|
||||
else:
|
||||
if node.containment.netkingdom_id != netkingdom_id:
|
||||
errors.append(f"{path}.containment.netkingdom_id must match netkingdom.id")
|
||||
_validate_fabric_ref(errors, f"{path}.containment.fabric_id", node.containment.fabric_id, fabric_kinds, "Fabric")
|
||||
if node.containment.subfabric_id:
|
||||
_validate_fabric_ref(
|
||||
errors,
|
||||
f"{path}.containment.subfabric_id",
|
||||
node.containment.subfabric_id,
|
||||
fabric_kinds,
|
||||
"Subfabric",
|
||||
)
|
||||
if node.ownership is None:
|
||||
errors.append(f"{path}.ownership must be an object for v1alpha2 exports")
|
||||
else:
|
||||
_validate_actor_ref(errors, f"{path}.ownership.owner_actor_id", node.ownership.owner_actor_id, actor_roles)
|
||||
if actor_roles.get(node.ownership.owner_actor_id) not in {node.ownership.owner_role, ""}:
|
||||
errors.append(f"{path}.ownership.owner_role does not match referenced actor role")
|
||||
if node.evidence is None:
|
||||
errors.append(f"{path}.evidence must be an object for v1alpha2 exports")
|
||||
elif (
|
||||
node.evidence.review_state == "accepted"
|
||||
and node.ownership
|
||||
and node.ownership.resolution not in {"explicit", "inherited"}
|
||||
):
|
||||
errors.append(f"{path}.ownership.resolution must be explicit or inherited for accepted nodes")
|
||||
|
||||
for index, edge in enumerate(export.edges):
|
||||
path = f"edges[{index}]"
|
||||
if edge.from_graph_id not in node_ids:
|
||||
errors.append(f"{path}.from references unknown node {edge.from_graph_id!r}")
|
||||
if edge.to_graph_id not in node_ids:
|
||||
errors.append(f"{path}.to references unknown node {edge.to_graph_id!r}")
|
||||
if edge.relationship_category is None:
|
||||
errors.append(f"{path}.relationship_category is required for v1alpha2 exports")
|
||||
if edge.evidence is None:
|
||||
errors.append(f"{path}.evidence must be an object for v1alpha2 exports")
|
||||
if edge.edge_type == "provides_utility_to" and edge.relationship_category != "utility":
|
||||
errors.append(f"{path}.relationship_category must be 'utility' for provides_utility_to edges")
|
||||
if edge.relationship_category == "utility":
|
||||
if edge.provider is None:
|
||||
errors.append(f"{path}.provider is required for utility edges")
|
||||
else:
|
||||
_validate_utility_side(errors, f"{path}.provider", edge.provider, actor_roles, fabric_kinds)
|
||||
if edge.consumer is None:
|
||||
errors.append(f"{path}.consumer is required for utility edges")
|
||||
else:
|
||||
_validate_utility_side(errors, f"{path}.consumer", edge.consumer, actor_roles, fabric_kinds)
|
||||
if edge.boundary is None:
|
||||
errors.append(f"{path}.boundary is required for utility edges")
|
||||
if edge.utility is None:
|
||||
errors.append(f"{path}.utility is required for utility edges")
|
||||
return errors
|
||||
|
||||
|
||||
def _validate_actor_ref(
|
||||
errors: list[str],
|
||||
path: str,
|
||||
actor_id: str,
|
||||
actor_roles: dict[str, str],
|
||||
) -> None:
|
||||
if actor_id not in actor_roles:
|
||||
errors.append(f"{path} references unknown actor {actor_id!r}")
|
||||
|
||||
|
||||
def _validate_fabric_ref(
|
||||
errors: list[str],
|
||||
path: str,
|
||||
fabric_id: str,
|
||||
fabric_kinds: dict[str, str],
|
||||
expected_kind: str,
|
||||
) -> None:
|
||||
actual_kind = fabric_kinds.get(fabric_id)
|
||||
if actual_kind is None:
|
||||
errors.append(f"{path} references unknown fabric {fabric_id!r}")
|
||||
elif actual_kind != expected_kind:
|
||||
errors.append(f"{path} must reference a {expected_kind}")
|
||||
|
||||
|
||||
def _validate_utility_side(
|
||||
errors: list[str],
|
||||
path: str,
|
||||
side: Any,
|
||||
actor_roles: dict[str, str],
|
||||
fabric_kinds: dict[str, str],
|
||||
) -> None:
|
||||
_validate_actor_ref(errors, f"{path}.owner_actor_id", side.owner_actor_id, actor_roles)
|
||||
if side.fabric_id not in fabric_kinds:
|
||||
errors.append(f"{path}.fabric_id references unknown fabric {side.fabric_id!r}")
|
||||
if side.subfabric_id:
|
||||
_validate_fabric_ref(errors, f"{path}.subfabric_id", side.subfabric_id, fabric_kinds, "Subfabric")
|
||||
|
||||
|
||||
def _canon_metadata_errors(export: FabricGraphExportPayload) -> list[str]:
|
||||
errors: list[str] = []
|
||||
for index, node in enumerate(export.nodes):
|
||||
|
||||
Reference in New Issue
Block a user