5,218,750 AI resources indexedWorking toward the world’s largest AI resource index.

PDF

fabric-ingest

Ingest local staged files (CSV, Parquet, JSON, Excel) into a Microsoft Fabric Lakehouse as Bronze Delta tables. Use when loading data from the target repo's data/sandbox/ into the Bronze layer. Handles sanitization, lineage envelope injection, and idempotent partition overwrite.

Installable skillMITBy scardoso-luUpdated Oct 31, 2026
INTENDED FOROpenClawInstall in a listed agent.

instructions.md

---
name: fabric-ingest-e79a995c41d1
description: Ingest local staged files (CSV, Parquet, JSON, Excel) into a Microsoft Fabric Lakehouse as Bronze Delta tables. Use when loading data from the target repo's data/sandbox/ into the Bronze layer. Handles sanitization, lineage envelope injection, and idempotent partition overwrite.
---

# fabric-ingest

## Separation of concerns — non-negotiable

Each source produces **three notebooks**:

| Notebook | Naming | Job |
|---|---|---|
| Download | `download_<source>.py` | Call source API → skip existing sandbox files → save raw files as-is. No Spark. No Delta writes. |
| Ingestion | `bronze_<source>.py` | Read sandbox files → compare against Bronze Delta table → process only new files → MERGE/partition-overwrite. Never full-overwrite. |
| Data quality | `dq_bronze_<source>.py` | Run Great Expectations checks on the Bronze table. Fail the job if checks fail. |

**Never combine these responsibilities.** A single notebook that downloads + ingests + overwrites is always wrong.

## MUST

- Read source files from `data/sandbox/` in this repository. Never read from live databases or APIs directly.
- Apply sanitization barrier: load to RAM → mask/redact in RAM → write to Delta
- Inject lineage envelope on every record: `_ingest_timestamp`, `_source_system`, `_batch_id`, `_ingest_date`
- Use `mergeSchema=True` for schema evolution
- Use `replaceWhere` (partition overwrite) for idempotent re-runs — write all rows, not a filtered subset
- **Source contracts are Python `@dataclass` embedded in the notebook** (`# %% [contract]` cell) — never YAML files
- **Downloads are Python notebooks** (`download_sources.py`) — never `.sh` scripts; use `mssparkutils` detection for Fabric vs local
- **Thresholds belong in the DQ notebook's parameter cell**, not in the ingestion notebook
- **`download_<source>` must skip existing sandbox files** — check by filename before downloading; never re-download a file already present in `Files/data/sandbox/<topic>/`
- **`bronze_<source>` must process only new files** — compare sandbox filenames against records already in the Bronze Delta table; do not reprocess files already ingested
- **`bronze_<source>` must use MERGE or partition-overwrite** — never full-overwrite the Bronze table; use `replaceWhere` or Delta MERGE so existing partitions are preserved

## PREFER

- Append-only writes to Bronze (never UPDATE or DELETE)
- Partition by `_ingest_date` for tables above ~100k rows
- Delta Lake over raw files (ACID transactions, compression, schema enforcement)
- All columns as `StringType` at Bronze — Silver is responsible for casting
- Read column types explicitly — never trust CSV type inference

## AVOID

- Reading from any network path, database, or API endpoint
- Writing raw unmasked data to any Delta table (sanitize first, always)
- Any filtering, splitting, or conditional writes based on data quality — that belongs in the DQ notebook
- `df.show()` or `print(df)` with sensitive columns
- Absolute paths like `/lakehouse/default/Files/...` — Fabric's `mssparkutils` rejects them; use lakehouse-relative paths: `Files/data/sandbox/...`

## Source organisation

Notebooks live under `workspace/<topic>/` — one subfolder per data source or business domain. The agent picks the folder name from the data subject (snake_case, short, descriptive — e.g. `lux_energy_price`, `lux_residence`). Stems must be unique across all subfolders because Fabric display names are flat.

```
workspace/
  lux_energy_price/
    download_electricity_day_ahead_prices.py
    bronze_electricity_day_ahead_prices.py
    dq_bronze_electricity_day_ahead_prices.py
  lux_residence/
    download_population_stats.py
    bronze_population_stats.py
    dq_bronze_population_stats.py
```

## Notebook cell structure

```
# %% [parameters]      — source path override, ingest date override
# %% [contract]        — BronzeContract @dataclass
# %% [imports]         — os, uuid, datetime, pyspark imports
# %% [schemas]         — StructType definitions (all StringType)
# %% [helpers]         — add_lineage_envelope(), write_bronze()
# %% [ingest]          — read → sanitize → envelope → write
# %% [summary]         — print row counts, batch_id, table path
```

## Key Pattern

```python
# %% [parameters]
SOURCE_PATH: str = ""        # blank = resolved from env
INGEST_DATE_OVERRIDE: str = ""

# %% [contract]
from dataclasses import dataclass

@dataclass(frozen=True)
class BronzeContract:
    source_system: str
    grain: str
    primary_keys: list[str]
    bronze_table: str
    sensitive_fields: list[str]
    sensitivity: str

CONTRACT = BronzeContract(
    source_system="ORDERS",
    grain="one order line item",
    primary_keys=["order_id"],
    bronze_table="raw_orders",
    sensitive_fields=[],
    sensitivity="internal",
)

# %% [imports]
import os, uuid
from datetime import datetime, timezone, date
from pyspark.sql import SparkSession, DataFrame
from pyspark.sql import functions as F
from pyspark.sql.types import StructType, StructField, StringType

spark = SparkSession.builder.getOrCreate()
BATCH_ID = str(uuid.uuid4())
INGEST_TS = datetime.now(timezone.utc)
INGEST_DATE = (
    datetime.fromisoformat(INGEST_DATE_OVERRIDE).date()
    if INGEST_DATE_OVERRIDE else INGEST_TS.date()
)
SRC = SOURCE_PATH or "Files/data/sandbox/orders.csv"  # lakehouse-relative, not /lakehouse/default/...

# %% [schemas]
SCHEMA = StructType([
    StructField("order_id",    StringType(), True),
    StructField("customer_id", StringType(), True),
    StructField("amount",      StringType(), True),
    StructField("order_date",  StringType(), True),
])

# %% [helpers]
def add_lineage_envelope(df: DataFrame) -> DataFrame:
    return (df
        .withColumn("_ingest_timestamp", F.lit(INGEST_TS).cast("timestamp"))
        .withColumn("_source_system",    F.lit(CONTRACT.source_system))
        .withColumn("_batch_id",         F.lit(BATCH_ID))
        .withColumn("_ingest_date",      F.lit(str(INGEST_DATE)).cast("date"))
    )

def write_bronze(df: DataFrame) -> None:
    (df.write
        .format("delta")
        .option("mergeSchema", "true")
        .option("replaceWhere", f"_ingest_date = '{INGEST_DATE}'")
        .partitionBy("_ingest_date")
        .mode("overwrite")
        .saveAsTable(CONTRACT.bronze_table)
    )

# %% [ingest]
df = spark.read.format("csv").options(header=True, enforceSchema=True).schema(SCHEMA).load(SRC)
df = add_lineage_envelope(df)
write_bronze(df)

# %% [summary]
print(f"[OK] {CONTRACT.bronze_table}: {df.count()} rows written")
print(f"     batch_id={BATCH_ID}  ingest_date={INGEST_DATE}")
print(f"     table={CONTRACT.bronze_table}")
```

## Supported Sources

| Format | How to read |
|---|---|
| CSV | `spark.read.format("csv").options(header=True).schema(schema).load(path)` |
| Parquet | `spark.read.format("parquet").load(path)` |
| GeoJSON | `spark.read.format("json").option("multiLine","true").load(path)` then `explode(col("features"))` |
| Excel | `pandas.read_excel()` → `spark.createDataFrame(pd_df)` |

## Multi-notebook pipelines

When a `download_sources.py` notebook fetches files that the Bronze notebook then reads, the paths **must match exactly**. Use a single shared constant so the match is guaranteed in code, not by coincidence:

```python
# Both download_sources.py and bronze_<source>.py — same value in both files
FABRIC_STAGING_DIR = "Files/data/sandbox/<topic>/xml"
LOCAL_STAGING_DIR  = "data/sandbox/<topic>/xml"
```

Use `mssparkutils` detection to resolve the active path at runtime:

```python
try:
    from notebookutils import mssparkutils as mss
    IS_FABRIC = True
except ImportError:
    IS_FABRIC = False

OUTPUT_DIR = FABRIC_STAGING_DIR if IS_FABRIC else LOCAL_STAGING_DIR
```

After any change to a staging directory constant, verify consistency before building. Upload the topic's notebooks to the `pipeline_lineage_check` MCP tool:

```python
pipeline_lineage_check(
    notebooks={
        "workspace/<topic>/bronze_<source>.py": "<file content>",
        "workspace/<topic>/silver_<source>.py": "<file content>",
        # …
    },
    topic="<topic>",  # optional
)
```

A path mismatch produces a silent empty read — no error, just missing data. A FAIL must be fixed before building or deploying. The tool returns the full validator output and any Python traceback.

## Non-standard packages

The Fabric Spark runtime does not include packages like `great_expectations`, `requests`, or `lxml`. Add a `%pip install` cell as the **first cell** of any notebook that needs them:

```python
# %% [install]
%pip install "great_expectations>=1.0,<2.0"
```

This works when the notebook is API-triggered because `deploy.py` sets `_inlineInstallationEnabled` automatically.

## Mock Data

Use the **mock-data** skill to generate a synthetic `data/sandbox/<topic>.csv` before building the notebook pipeline.

Discuss this resource

Platform guidance

Imported from the listed AgentSkill.sh category. Complete supplied package files and notices are retained. Required external tools and credentials are not installed. No bundled code was executed during import. Individual agent task behavior has not been tested.