DBT: S3 Inventory Snapshots
DBT model name: s3_inventory_snapshots
Explore dependencies/lineage: link
Description
History-keeping table of weekly S3-Inventory snapshots for trase-storage (one row per snapshot × object). Excluded prefixes are dropped at ingest. First run seeds with the latest snapshot; history accrues forward.
Details
| Column | Type | Description |
|---|---|---|
snapshot_ts |
`` | Inventory run directory name, e.g. 2026-05-31T01-00Z (UTC). |
key |
`` | Object key within the bucket. |
size_bytes |
`` | Object size in bytes. |
last_modified |
`` | Object LastModifiedDate from the inventory (UTC). |
etag |
`` | Object ETag at snapshot time. |
bucket |
`` | Source bucket (trase-storage). |
Review full report including sample errors records if they exist (link)
| Test column | Test name | Failing rows | Last test run | Last status | Query in Metabase |
|---|---|---|---|---|---|
snapshot_ts |
dbt_expectations_expect_row_values_to_have_recent_data_s3_inventory_snapshots_strptime_snapshot_ts_Y_m_dT_H_MZ___day__9 |
— | 2026-07-19 06:50 | pass |
Models
s3_monitor_excluded_prefixes
No called script or script source not found.
"""Weekly S3-Inventory snapshots for the `trase-storage` bucket.
Incremental, history-keeping table: one row per (snapshot, object). Each weekly
run appends any inventory snapshots not already loaded, so the table accumulates
a point-in-time history of every file in the bucket.
First run seeds with only the most recent snapshot; history then accrues forward
on each subsequent run. (Old snapshots are not back-filled — that would mean
re-reading every historical full-bucket listing.)
Two layers of exclusion keep the catalog to the ~tens-of-thousands of files we
care about (vs. ~3M in the raw bucket):
* static: any key matching a glob in the `s3_monitor_excluded_prefixes` seed;
* dynamic: any key whose direct (non-recursive) parent prefix has more than
`max_direct_children` file children — drops auto-generated dumps of
thousands of sibling files.
Directory-marker keys (ending in `/`) are dropped entirely.
"""
import fnmatch
import pandas as pd
def _exclude_mask(keys: pd.Series, patterns) -> pd.Series:
"""Vectorised fnmatch: True where a key matches any exclusion glob."""
patterns = [p for p in patterns if isinstance(p, str) and p]
if not patterns:
return pd.Series(False, index=keys.index)
regex = "|".join(f"(?:{fnmatch.translate(p)})" for p in patterns)
return keys.str.match(regex)
def _parent_prefix(keys: pd.Series) -> pd.Series:
"""Direct parent 'directory' of each key ('' for root-level files)."""
return keys.str.rpartition("/")[0]
def _process_snapshot(df, snapshot_ts, patterns, max_direct_children, out_cols):
"""Filter + type a single snapshot's inventory frame.
Done per-snapshot (inside the load loop) instead of on one giant
concatenated frame, so peak memory stays ~one snapshot rather than the full
multi-snapshot history. Both exclusion passes are per-snapshot by nature
(static globs are row-wise; the direct-children cap is scoped to a single
snapshot's listing), so the result is identical to filtering after a concat.
Returns the typed `out_cols` frame, or None if nothing survives.
"""
# Drop directory-marker keys (S3 inventory lists "foo/" with size 0).
df = df[~df["key"].str.endswith("/")]
# Static exclusions (seed globs).
if patterns:
df = df[~_exclude_mask(df["key"], patterns)]
df = df.copy()
# Dynamic exclusion: parents with too many direct file children.
if not df.empty and max_direct_children > 0:
child_counts = df.groupby(_parent_prefix(df["key"]))["key"].transform("size")
df = df[child_counts.values <= max_direct_children].copy()
if df.empty:
return None
df["snapshot_ts"] = snapshot_ts
df["size_bytes"] = pd.to_numeric(df["size"], errors="coerce").astype("Int64")
df["size_mb"] = (df["size_bytes"] / (1024 * 1024)).round(3)
df["last_modified"] = pd.to_datetime(df["modified"], errors="coerce", utc=True)
df = df.rename(columns={"hash": "etag"})
return df[out_cols]
def model(dbt, session):
dbt.config(
materialized="incremental",
unique_key=["snapshot_ts", "key"],
tags=["s3_inventory"],
)
from trase.tools.aws.s3_helpers import (
list_s3_inventory_manifests,
read_s3_inventory_manifest,
)
# Tunables come from the model's +meta in dbt_project.yml — see note in
# s3_file_schemas.
meta = dbt.config.get("meta", {})
max_direct_children = int(meta.get("max_direct_children", 200))
out_cols = [
"bucket",
"key",
"size_bytes",
"size_mb",
"last_modified",
"etag",
"snapshot_ts",
]
# ------------------------------------------------------------------ source
manifests = list_s3_inventory_manifests()
if not manifests:
raise ValueError("No S3 inventory manifests found under the inventory prefix.")
existing_ts: set[str] = set()
if dbt.is_incremental:
rows = session.execute(
f"SELECT DISTINCT snapshot_ts FROM {dbt.this}"
).fetchall()
existing_ts = {r[0] for r in rows}
todo = (
[(ts, k) for ts, k in manifests if ts not in existing_ts]
if existing_ts
else manifests[-1:] # first load: latest snapshot only
)
print(f"[s3_inventory_snapshots] loading {len(todo)} snapshot(s)", flush=True)
# Load the exclusion globs once (not per snapshot). NOTE: keep
# `dbt.ref(...).df()` on its own line — dbt's Python ref parser fails to
# register the ref if a subscript (`[...]`) is chained directly onto it.
excluded = dbt.ref("s3_monitor_excluded_prefixes").df()
exclusion_patterns = excluded["prefix_glob"].to_list()
# Read + filter + type each snapshot as we go, so only the kept
# (post-exclusion) rows accumulate. Peak memory is ~one snapshot's raw
# listing rather than the whole concatenated history.
frames = []
kept = 0
for i, (snapshot_ts, manifest_key) in enumerate(todo, start=1):
print(
f"[s3_inventory_snapshots] snapshot {i}/{len(todo)}: {snapshot_ts}",
flush=True,
)
df = read_s3_inventory_manifest(manifest_key, progress=True)
if df.empty:
continue
df = _process_snapshot(
df, snapshot_ts, exclusion_patterns, max_direct_children, out_cols
)
if df is not None and not df.empty:
frames.append(df)
kept += len(df)
if not frames:
return pd.DataFrame(columns=out_cols)
print(
f"[s3_inventory_snapshots] {kept:,} rows after exclusions "
f"across {len(frames)} snapshot(s); writing",
flush=True,
)
return pd.concat(frames, ignore_index=True)[out_cols]
-
Dbt path:
trase_ducklake.main_s3_monitoring.s3_inventory_snapshots -
Containing yaml link: trase/data_pipeline/models/s3_monitoring/_schema_s3_monitoring.yml
-
Model file: trase/data_pipeline/models/s3_monitoring/s3_inventory_snapshots.py
-
Tags:
s3_inventory