fix Fabric authority import compatibility
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
903a2fb090
commit
cf40c7bb4e
5 changed files with 87 additions and 4 deletions
|
|
@ -23,7 +23,7 @@ class FabricGraphImport(Base, TimestampMixin):
|
||||||
)
|
)
|
||||||
source_repo_slug: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
source_repo_slug: Mapped[str] = mapped_column(String(100), nullable=False, index=True)
|
||||||
source_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
source_url: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
source_commit: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True)
|
source_commit: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
|
||||||
source_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
source_path: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
api_version: Mapped[str | None] = mapped_column(String(100), 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)
|
schema_version: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True)
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,10 @@ class FabricGraphSource(BaseModel):
|
||||||
repo: str | None = None
|
repo: str | None = None
|
||||||
producer: str | None = None
|
producer: str | None = None
|
||||||
registry: str | None = None
|
registry: str | None = None
|
||||||
commit: str | None = None
|
# Accepted-snapshot-set revisions include the algorithm and digest, for
|
||||||
|
# example ``snapshot-set:sha256:<64 hex chars>``. They are intentionally
|
||||||
|
# longer than a Git SHA but still bounded before persistence.
|
||||||
|
commit: str | None = Field(default=None, max_length=255)
|
||||||
path: str | None = None
|
path: str | None = None
|
||||||
generation_reason: str | None = None
|
generation_reason: str | None = None
|
||||||
|
|
||||||
|
|
@ -98,6 +101,18 @@ class FabricGraphAccountingPayload(BaseModel):
|
||||||
valid_until: str | None = None
|
valid_until: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class FabricGraphDeploymentOverlayPayload(BaseModel):
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
deployment_environment: str | None = None
|
||||||
|
deployment_scenario: str | None = None
|
||||||
|
routing_authority: str | None = None
|
||||||
|
access_zone: str | None = None
|
||||||
|
policy_authority: str | None = None
|
||||||
|
exposure_class: str | None = None
|
||||||
|
route_evidence: dict[str, str | int | float | bool | None] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class FabricGraphEvidencePayload(BaseModel):
|
class FabricGraphEvidencePayload(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
|
|
@ -154,6 +169,7 @@ class FabricGraphNodePayload(BaseModel):
|
||||||
containment: FabricGraphContainmentPayload | None = None
|
containment: FabricGraphContainmentPayload | None = None
|
||||||
ownership: FabricGraphOwnershipPayload | None = None
|
ownership: FabricGraphOwnershipPayload | None = None
|
||||||
accounting: FabricGraphAccountingPayload | None = None
|
accounting: FabricGraphAccountingPayload | None = None
|
||||||
|
deployment_overlay: FabricGraphDeploymentOverlayPayload | None = None
|
||||||
evidence: FabricGraphEvidencePayload | None = None
|
evidence: FabricGraphEvidencePayload | None = None
|
||||||
canon_category: str | None = None
|
canon_category: str | None = None
|
||||||
canon_anchor: str | None = None
|
canon_anchor: str | None = None
|
||||||
|
|
@ -180,6 +196,7 @@ class FabricGraphEdgePayload(BaseModel):
|
||||||
boundary: FabricGraphBoundaryPayload | None = None
|
boundary: FabricGraphBoundaryPayload | None = None
|
||||||
utility: FabricGraphUtilityPayload | None = None
|
utility: FabricGraphUtilityPayload | None = None
|
||||||
accounting: FabricGraphAccountingPayload | None = None
|
accounting: FabricGraphAccountingPayload | None = None
|
||||||
|
deployment_overlay: FabricGraphDeploymentOverlayPayload | None = None
|
||||||
evidence: FabricGraphEvidencePayload | None = None
|
evidence: FabricGraphEvidencePayload | None = None
|
||||||
attributes: dict[str, Any] = Field(default_factory=dict)
|
attributes: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,10 @@ async def _record_invalid_import(
|
||||||
import_run = FabricGraphImport(
|
import_run = FabricGraphImport(
|
||||||
source_repo_slug=source_repo_slug,
|
source_repo_slug=source_repo_slug,
|
||||||
source_url=source_url,
|
source_url=source_url,
|
||||||
source_commit=_source_value(payload, "commit"),
|
# Invalid payloads are retained for diagnostics, but their
|
||||||
|
# denormalized provenance must not make the rejection path fail.
|
||||||
|
# The complete input remains available in ``graph_json``.
|
||||||
|
source_commit=_bounded_source_value(payload, "commit", max_length=255),
|
||||||
source_path=_source_value(payload, "path"),
|
source_path=_source_value(payload, "path"),
|
||||||
api_version=str(payload.get("apiVersion")) if payload.get("apiVersion") else None,
|
api_version=str(payload.get("apiVersion")) if payload.get("apiVersion") else None,
|
||||||
export_kind=str(payload.get("kind")) if payload.get("kind") else None,
|
export_kind=str(payload.get("kind")) if payload.get("kind") else None,
|
||||||
|
|
@ -620,6 +623,13 @@ def _source_value(payload: dict[str, Any], field: str) -> str | None:
|
||||||
return str(value) if value else None
|
return str(value) if value else None
|
||||||
|
|
||||||
|
|
||||||
|
def _bounded_source_value(
|
||||||
|
payload: dict[str, Any], field: str, *, max_length: int
|
||||||
|
) -> str | None:
|
||||||
|
value = _source_value(payload, field)
|
||||||
|
return value[:max_length] if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
def _parse_datetime(value: Any) -> datetime | None:
|
def _parse_datetime(value: Any) -> datetime | None:
|
||||||
if not isinstance(value, str) or not value:
|
if not isinstance(value, str) or not value:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
"""accept deterministic Fabric snapshot-set revisions
|
||||||
|
|
||||||
|
Revision ID: b5e6f7a8c9d0
|
||||||
|
Revises: a4d5e6f7b8c9
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "b5e6f7a8c9d0"
|
||||||
|
down_revision = "a4d5e6f7b8c9"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
"fabric_graph_imports",
|
||||||
|
"source_commit",
|
||||||
|
existing_type=sa.String(length=80),
|
||||||
|
type_=sa.String(length=255),
|
||||||
|
existing_nullable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.alter_column(
|
||||||
|
"fabric_graph_imports",
|
||||||
|
"source_commit",
|
||||||
|
existing_type=sa.String(length=255),
|
||||||
|
type_=sa.String(length=80),
|
||||||
|
existing_nullable=True,
|
||||||
|
postgresql_using="left(source_commit, 80)",
|
||||||
|
)
|
||||||
|
|
@ -1291,7 +1291,8 @@ def _financial_fabric_graph_export(generated_at="2026-05-24T00:00:00Z"):
|
||||||
"source": {
|
"source": {
|
||||||
"producer": "railiance-fabric",
|
"producer": "railiance-fabric",
|
||||||
"registry": "registry",
|
"registry": "registry",
|
||||||
"commit": "financial-example",
|
"commit": "snapshot-set:sha256:" + ("a" * 64),
|
||||||
|
"path": "registry://accepted-snapshots",
|
||||||
"generation_reason": "operator_refresh",
|
"generation_reason": "operator_refresh",
|
||||||
},
|
},
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
|
|
@ -1373,6 +1374,11 @@ def _financial_fabric_graph_export(generated_at="2026-05-24T00:00:00Z"):
|
||||||
"cost_center_id": "cc.platform.shared",
|
"cost_center_id": "cc.platform.shared",
|
||||||
"allocation_model": "direct",
|
"allocation_model": "direct",
|
||||||
},
|
},
|
||||||
|
"deployment_overlay": {
|
||||||
|
"deployment_environment": "local",
|
||||||
|
"routing_authority": "ops-hub",
|
||||||
|
"route_evidence": {"observed": True, "port": 8000},
|
||||||
|
},
|
||||||
"evidence": {
|
"evidence": {
|
||||||
"state": "declared",
|
"state": "declared",
|
||||||
"review_state": "accepted",
|
"review_state": "accepted",
|
||||||
|
|
@ -1581,6 +1587,8 @@ class TestFabricGraphReadModel:
|
||||||
body = r.json()
|
body = r.json()
|
||||||
assert body["import_run"]["api_version"] == "railiance.fabric/v1alpha2"
|
assert body["import_run"]["api_version"] == "railiance.fabric/v1alpha2"
|
||||||
assert body["import_run"]["schema_version"] == "financial-fabric-v1"
|
assert body["import_run"]["schema_version"] == "financial-fabric-v1"
|
||||||
|
assert body["import_run"]["source_commit"] == "snapshot-set:sha256:" + ("a" * 64)
|
||||||
|
assert body["import_run"]["source_path"] == "registry://accepted-snapshots"
|
||||||
assert body["import_run"]["netkingdom_id"] == "railiance.netkingdom"
|
assert body["import_run"]["netkingdom_id"] == "railiance.netkingdom"
|
||||||
assert body["import_run"]["actor_count"] == 3
|
assert body["import_run"]["actor_count"] == 3
|
||||||
assert body["import_run"]["fabric_count"] == 2
|
assert body["import_run"]["fabric_count"] == 2
|
||||||
|
|
@ -1646,6 +1654,19 @@ class TestFabricGraphReadModel:
|
||||||
r = await client.get("/fabric/graph/nodes")
|
r = await client.get("/fabric/graph/nodes")
|
||||||
assert r.status_code == 404
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
async def test_invalid_export_with_oversized_revision_is_recorded_not_500(self, client):
|
||||||
|
payload = _financial_fabric_graph_export()
|
||||||
|
payload["source"]["commit"] = "x" * 300
|
||||||
|
|
||||||
|
r = await client.post("/fabric/graph-exports", json=payload)
|
||||||
|
|
||||||
|
assert r.status_code == 422, r.text
|
||||||
|
import_id = r.json()["detail"]["import_id"]
|
||||||
|
r = await client.get("/fabric/graph-exports?validation_status=invalid")
|
||||||
|
invalid = next(item for item in r.json() if item["id"] == import_id)
|
||||||
|
assert invalid["source_commit"] == "x" * 255
|
||||||
|
assert "at most 255 characters" in invalid["error_details"]["error"]
|
||||||
|
|
||||||
async def test_legacy_fabric_exports_remain_compatible_with_null_financial_fields(self, client):
|
async def test_legacy_fabric_exports_remain_compatible_with_null_financial_fields(self, client):
|
||||||
r = await client.post("/fabric/graph-exports", json=_fabric_graph_export())
|
r = await client.post("/fabric/graph-exports", json=_fabric_graph_export())
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue