Verify Iceberg projections and catalog health#

Goal. Work through a bounded, reproducible example and inspect the evidence before connecting an external service.

Prerequisites. Base FraudTwin install. Optional extras and Docker commands are clearly marked.

Produces. Tables, fingerprints, manifests, and verification output.

Source size. The default cells generate approximately 1,000 logical payments; increase duration and population together for a 10,000-payment run.

Offline path. All marked offline cells run without Docker or network services. Service cells are optional and explicitly marked in notebook metadata.

Cleanup. Outputs are written under a temporary directory; remove any local run directory if you changed the output location.

!pip install pyiceberg[s3fs]
!pip install prometheus-client requests

# Launch the services for this tutorial. Re-running this cell is safe.
import os

os.environ.setdefault("GRAFANA_ADMIN_PASSWORD", "fraudtwin-local")
compose_profiles = "--profile lakehouse --profile observability"
!docker compose {compose_profiles} up -d minio minio-init iceberg-rest prometheus grafana
!docker compose --profile lakehouse --profile observability ps
Requirement already satisfied: pyiceberg[s3fs] in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (0.12.0)
Requirement already satisfied: mmh3>=4.0.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (5.3.0)
Requirement already satisfied: requests>=2.20.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2.34.2)
Requirement already satisfied: click>=7.1.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (8.5.0)
Requirement already satisfied: rich>=10.11.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (15.0.0)
Requirement already satisfied: strictyaml>=1.7.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (1.7.3)
Requirement already satisfied: pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2.13.5)
Requirement already satisfied: fsspec>=2023.1.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2026.9.0)
Requirement already satisfied: pyparsing>=3.1.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (3.3.3)
Requirement already satisfied: tenacity>=8.2.3 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (9.1.4)
Requirement already satisfied: pyroaring>=1.0.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (1.1.0)
Requirement already satisfied: cachetools>=5.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (7.2.0)
Requirement already satisfied: zstandard>=0.13.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (0.25.0)
Requirement already satisfied: s3fs>=2023.1.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pyiceberg[s3fs]) (2026.9.0)
Requirement already satisfied: annotated-types>=0.6.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (0.8.0)
Requirement already satisfied: pydantic-core==2.46.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (2.46.5)
Requirement already satisfied: typing-extensions>=4.14.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (4.16.0)
Requirement already satisfied: typing-inspection>=0.4.2 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from pydantic!=2.12.0,!=2.12.1,!=2.4.0,!=2.4.1,<3.0,>=2.0->pyiceberg[s3fs]) (0.4.4)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (3.5.1)
Requirement already satisfied: idna<4,>=2.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (3.19)
Requirement already satisfied: urllib3<3,>=1.26 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests>=2.20.0->pyiceberg[s3fs]) (2026.7.22)
Requirement already satisfied: markdown-it-py>=2.2.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from rich>=10.11.0->pyiceberg[s3fs]) (3.0.0)
Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from rich>=10.11.0->pyiceberg[s3fs]) (2.21.0)
Requirement already satisfied: mdurl~=0.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->pyiceberg[s3fs]) (0.1.2)
Requirement already satisfied: aiobotocore<4.0.0,>=2.19.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from s3fs>=2023.1.0->pyiceberg[s3fs]) (3.9.1)
Requirement already satisfied: aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from s3fs>=2023.1.0->pyiceberg[s3fs]) (3.14.3)
Requirement already satisfied: aioitertools<1.0.0,>=0.5.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (0.13.0)
Requirement already satisfied: botocore<1.43.76,>=1.43.66 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.43.75)
Requirement already satisfied: jmespath<2.0.0,>=0.7.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.1.0)
Requirement already satisfied: multidict<7.0.0,>=6.0.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (6.9.1)
Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (2.9.0.post0)
Requirement already satisfied: wrapt<3.0.0,>=1.10.10 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (2.4.1)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (2.7.1)
Requirement already satisfied: aiosignal>=1.4.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.4.0)
Requirement already satisfied: attrs>=17.3.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (26.1.0)
Requirement already satisfied: frozenlist>=1.1.1 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.8.0)
Requirement already satisfied: propcache>=0.2.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (0.5.4)
Requirement already satisfied: yarl<2.0,>=1.17.0 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from aiohttp!=4.0.0a0,!=4.0.0a1,>=3.9.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.25.1)
Requirement already satisfied: six>=1.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from python-dateutil<3.0.0,>=2.1->aiobotocore<4.0.0,>=2.19.0->s3fs>=2023.1.0->pyiceberg[s3fs]) (1.17.0)
Requirement already satisfied: prometheus-client in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (0.26.0)
Requirement already satisfied: requests in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (2.34.2)
Requirement already satisfied: charset_normalizer<4,>=2 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (3.5.1)
Requirement already satisfied: idna<4,>=2.5 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (3.19)
Requirement already satisfied: urllib3<3,>=1.26 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (2.7.0)
Requirement already satisfied: certifi>=2023.5.7 in /home/emc/Projects/test/.venv/lib/python3.14/site-packages (from requests) (2026.7.22)
?25l[+] up 4/5
 ✔ Container fraudtwin-prometheus-1   Running                              0.0s
 ✔ Container fraudtwin-minio-1        Running                              0.0s
 ✔ Container fraudtwin-grafana-1      Running                              0.0s
 ✔ Container fraudtwin-iceberg-rest-1 Running                              0.0s
 ⠋ Container fraudtwin-minio-init-1   Starting                             0.0s
?25h?25l[+] up 4/5
 ✔ Container fraudtwin-prometheus-1   Running                              0.0s
 ✔ Container fraudtwin-minio-1        Running                              0.0s
 ✔ Container fraudtwin-grafana-1      Running                              0.0s
 ✔ Container fraudtwin-iceberg-rest-1 Running                              0.0s
 ⠙ Container fraudtwin-minio-init-1   Starting                             0.1s
?25h?25l[+] up 5/5
 ✔ Container fraudtwin-prometheus-1   Running                              0.0s
 ✔ Container fraudtwin-minio-1        Running                              0.0s
 ✔ Container fraudtwin-grafana-1      Running                              0.0s
 ✔ Container fraudtwin-iceberg-rest-1 Running                              0.0s
 ✔ Container fraudtwin-minio-init-1   Started                              0.2s
?25hNAME                           IMAGE                                                  COMMAND                  SERVICE           CREATED         STATUS         PORTS
fraudtwin-grafana-1           grafana/grafana:11.2.0                                 "/run.sh"                grafana           5 minutes ago   Up 5 minutes   127.0.0.1:3000->3000/tcp
fraudtwin-iceberg-rest-1      tabulario/iceberg-rest:1.6.0                           "java -jar iceberg-r…"   iceberg-rest      5 minutes ago   Up 5 minutes   0.0.0.0:8181->8181/tcp, [::]:8181->8181/tcp
fraudtwin-kafka-1             confluentinc/cp-kafka:7.7.1                            "/etc/confluent/dock…"   kafka             5 minutes ago   Up 5 minutes   0.0.0.0:9092->9092/tcp, [::]:9092->9092/tcp
fraudtwin-minio-1             docker.io/bitnamilegacy/minio:2025.7.23-debian-12-r3   "/opt/bitnami/script…"   minio             5 minutes ago   Up 5 minutes   0.0.0.0:9000-9001->9000-9001/tcp, [::]:9000-9001->9000-9001/tcp
fraudtwin-prometheus-1        prom/prometheus:v2.55.1                                "/bin/prometheus --c…"   prometheus        5 minutes ago   Up 5 minutes   127.0.0.1:9090->9090/tcp
fraudtwin-schema-registry-1   confluentinc/cp-schema-registry:7.7.1                  "/etc/confluent/dock…"   schema-registry   5 minutes ago   Up 5 minutes   0.0.0.0:8081->8081/tcp, [::]:8081->8081/tcp

Optional Iceberg and observability setup#

The offline cells below build deterministic Bronze/Silver projections. Run the optional integration setup cell below when you want this independent tutorial to launch the catalog, object store, Prometheus, and Grafana for service-backed checks or dashboard publishing.

docker compose --profile lakehouse --profile observability up -d minio minio-init iceberg-rest prometheus grafana

After the services start, the integration cell checks the Iceberg catalog, Prometheus, and Grafana health endpoints. The completed-run Grafana workflow is maintained in the lakehouse observability tutorial; use that notebook when you want dashboard panels and run-level metrics. Stop the services later with docker compose --profile lakehouse --profile observability down.

Grafana uses username admin and password fraudtwin-local for a fresh local volume. If that password was changed earlier, use the current one. Set FRAUDTWIN_ICEBERG_CATALOG_URI when using another REST catalog.

Set up a deterministic source run

from pathlib import Path

import polars as pl

from fraudtwin.config import load_config
from fraudtwin.generation import generate

root = next(
    (p for p in (Path.cwd(), *Path.cwd().parents) if (p / "configs" / "minimal.yaml").exists()),
    Path.cwd(),
)
base = load_config(root / "configs" / "minimal.yaml")
# Scale the population so the bounded example produces about 1,000 payments.
population = base.population.model_copy(
    update={
        "customers": 200,
        "accounts": 300,
        "cards": 240,
        "devices": 240,
        "pix_keys": 160,
        "merchants": 60,
    }
)
simulation = base.simulation.model_copy(update={"duration_days": 10})
fraud = base.fraud.model_copy(update={"enabled": True, "target_rate": 0.05})
config = base.model_copy(
    update={"population": population, "simulation": simulation, "fraud": fraud}
)
data = generate(config, write=False)
run_id = data.run_id
payments = pl.DataFrame([item.model_dump(mode="json") for item in data.behavior.payments])
print("Generated source run")
print(f"  run id: {run_id}")
print(f"  payments: {len(payments):,}")
print(f"  events: {len(data.behavior.payment_events):,}")
Generated source run
  run id: RUN-2a3ad02ee370aeb8
  payments: 1,092
  events: 4,699

Build the local Silver projection

from fraudtwin.lakehouse import build_bronze_records, logical_fingerprint, silver_rows

bronze = build_bronze_records(data.entities, data.behavior, data.manifest)
silver = silver_rows(bronze)
snapshot_a = logical_fingerprint(silver)
print("Silver projection")
print(f"  rows: {len(silver):,}")
print(f"  logical fingerprint: {snapshot_a[:12]}...")
Silver projection
  rows: 9,386
  logical fingerprint: 760720c0370f...

Compare two logical versions

snapshot_b = logical_fingerprint(silver[:-1])
display(
    pl.DataFrame(
        [
            {"version": "A", "rows": len(silver), "fingerprint": f"{snapshot_a[:12]}..."},
            {"version": "B", "rows": len(silver[:-1]), "fingerprint": f"{snapshot_b[:12]}..."},
        ]
    )
)
print(f"content changed: {snapshot_a != snapshot_b}")
shape: (2, 3)
versionrowsfingerprint
stri64str
"A"9386"760720c0370f..."
"B"9385"97e4783c49a7..."
content changed: True

Test a late-event backfill

backfill = silver_rows([*bronze, bronze[0]])
print("Late-event backfill")
print(f"  rows after deduplication: {len(backfill):,}")
print(f"  duplicate ignored: {len(backfill) == len(silver)}")
Late-event backfill
  rows after deduplication: 9,386
  duplicate ignored: True

Review the offline observability contract

slo = {"lag_seconds": 0, "duplicate_rate": 0.0, "invalid_records": 0, "reconciliation": "passed"}
print("Offline SLO contract (illustrative)")
display(pl.DataFrame([slo]))
Offline SLO contract (illustrative)
shape: (1, 4)
lag_secondsduplicate_rateinvalid_recordsreconciliation
i64f64i64str
00.00"passed"

Write a compact artifact and fingerprint

assert slo["reconciliation"] == "passed"
print(
    "Logical fingerprints change when a projection changes; real Iceberg snapshot IDs require "
    "materialize_run."
)
Logical fingerprints change when a projection changes; real Iceberg snapshot IDs require materialize_run.

Verify invariants and clean up

import os

try:
    import requests
    from pyiceberg.catalog import load_catalog

    catalog = load_catalog(
        "fraudtwin",
        type="rest",
        uri=os.getenv("FRAUDTWIN_ICEBERG_CATALOG_URI", "http://localhost:8181"),
    )
    namespaces = catalog.list_namespaces()
    prometheus = requests.get("http://localhost:9090/-/ready", timeout=3)
    metric = requests.get("http://localhost:9090/api/v1/query", params={"query": "up"}, timeout=3)
    grafana = requests.get("http://localhost:3000/api/health", timeout=3)
    checks = pl.DataFrame(
        [
            {
                "check": "Iceberg catalog",
                "status": "connected",
                "details": f"{len(namespaces)} namespaces",
            },
            {
                "check": "Prometheus",
                "status": f"HTTP {prometheus.status_code}",
                "details": "readiness endpoint",
            },
            {
                "check": "Prometheus query",
                "status": f"HTTP {metric.status_code}",
                "details": "up query",
            },
            {
                "check": "Grafana",
                "status": f"HTTP {grafana.status_code}",
                "details": "health endpoint",
            },
        ]
    )
    display(checks)
    print("Open Grafana: http://localhost:3000")
    print("Open Prometheus: http://localhost:9090")
except Exception as exc:
    print("Optional services are unavailable; offline snapshot analysis remains valid.")
    print("  offline_fallback: yes")
    print(f"  reason: {type(exc).__name__}")
shape: (4, 3)
checkstatusdetails
strstrstr
"Iceberg catalog""connected""0 namespaces"
"Prometheus""HTTP 200""readiness endpoint"
"Prometheus query""HTTP 200""up query"
"Grafana""HTTP 200""health endpoint"
Open Grafana: http://localhost:3000
Open Prometheus: http://localhost:9090