|
| 1 | +--- |
| 2 | +name: define-contracts |
| 3 | +description: > |
| 4 | + Define data contracts and pipeline validation with Pointblank. |
| 5 | + Covers Contract, Step, Schema, Pipeline, and PipelineResult for |
| 6 | + enforcing structural and semantic expectations at data boundaries. |
| 7 | + Use when setting up source/target contracts, pipeline validation, |
| 8 | + or contract serialization to YAML. |
| 9 | +license: MIT |
| 10 | +compatibility: Requires Python >=3.10, pointblank installed. |
| 11 | +metadata: |
| 12 | + author: rich-iannone |
| 13 | + version: "1.0" |
| 14 | + tags: |
| 15 | + - data-contracts |
| 16 | + - pipeline-validation |
| 17 | + - schema |
| 18 | + - data-quality |
| 19 | +--- |
| 20 | + |
| 21 | +# Define Contracts |
| 22 | + |
| 23 | +Skill for defining data contracts that enforce expectations at the |
| 24 | +boundaries of data pipelines. A contract declares what a dataset |
| 25 | +must look like (schema) and what properties it must satisfy (steps), |
| 26 | +along with metadata about ownership and violation behavior. |
| 27 | + |
| 28 | +## Quick start |
| 29 | + |
| 30 | +```python |
| 31 | +import pointblank as pb |
| 32 | + |
| 33 | +contract = pb.Contract( |
| 34 | + name="orders-source", |
| 35 | + direction="source", |
| 36 | + schema=pb.Schema( |
| 37 | + order_id="Int64", |
| 38 | + amount="Float64", |
| 39 | + status="String", |
| 40 | + ), |
| 41 | + steps=[ |
| 42 | + pb.Step("col_vals_gt", columns="amount", value=0), |
| 43 | + pb.Step("col_vals_not_null", columns="order_id"), |
| 44 | + pb.Step("col_vals_in_set", columns="status", |
| 45 | + set=["pending", "shipped", "delivered"]), |
| 46 | + ], |
| 47 | + on_violation="raise", |
| 48 | +) |
| 49 | + |
| 50 | +# Validate data against the contract |
| 51 | +validation = contract.validate(data=df) |
| 52 | +``` |
| 53 | + |
| 54 | +## Skill directory structure |
| 55 | + |
| 56 | +``` |
| 57 | +skills/define-contracts/ |
| 58 | ++-- SKILL.md <- This file |
| 59 | ++-- references/ |
| 60 | + +-- contract-reference.md <- Contract, Step, on_violation details |
| 61 | + +-- pipeline-reference.md <- Pipeline, PipelineResult details |
| 62 | +``` |
| 63 | + |
| 64 | +## When to use what |
| 65 | + |
| 66 | +| I want to... | Use | |
| 67 | +| ------------------------------------- | ---------------------- | |
| 68 | +| Declare expected table structure | `Schema` | |
| 69 | +| Declare a semantic check as a step | `Step` | |
| 70 | +| Bundle schema + steps into a contract | `Contract` | |
| 71 | +| Validate data at pipeline ingestion | `Pipeline` with source | |
| 72 | +| Validate data after transformation | `Pipeline` with target | |
| 73 | +| Validate both source and target | `Pipeline` with both | |
| 74 | +| Serialize a contract to YAML | `contract.to_yaml()` | |
| 75 | +| Load a contract from YAML | `Contract.from_yaml()` | |
| 76 | +| Warn on violation without stopping | `on_violation="warn"` | |
| 77 | +| Raise an exception on violation | `on_violation="raise"` | |
| 78 | +| Log violations silently | `on_violation="log"` | |
| 79 | + |
| 80 | +## Core concepts |
| 81 | + |
| 82 | +### Contract |
| 83 | + |
| 84 | +A `Contract` bundles: |
| 85 | + |
| 86 | +- **name** -- identifier for the contract |
| 87 | +- **direction** -- `"source"` (incoming data) or `"target"` (output) |
| 88 | +- **schema** -- expected column names and types |
| 89 | +- **steps** -- list of `Step` objects defining semantic checks |
| 90 | +- **on_violation** -- what to do when validation fails |
| 91 | + |
| 92 | +```python |
| 93 | +contract = pb.Contract( |
| 94 | + name="customer-data", |
| 95 | + direction="source", |
| 96 | + schema=pb.Schema( |
| 97 | + id="Int64", |
| 98 | + name="String", |
| 99 | + email="String", |
| 100 | + age="Int32", |
| 101 | + ), |
| 102 | + steps=[ |
| 103 | + pb.Step("col_vals_not_null", columns="id"), |
| 104 | + pb.Step("col_vals_gt", columns="age", value=0), |
| 105 | + pb.Step("col_vals_regex", columns="email", |
| 106 | + pattern=r".+@.+\..+"), |
| 107 | + pb.Step("rows_distinct", columns_subset=["id"]), |
| 108 | + ], |
| 109 | + version="1.0", |
| 110 | + owner="data-team", |
| 111 | + consumers=["analytics", "ml-pipeline"], |
| 112 | + description="Customer master data contract", |
| 113 | + on_violation="raise", |
| 114 | +) |
| 115 | +``` |
| 116 | + |
| 117 | +### Step |
| 118 | + |
| 119 | +A `Step` is a declarative representation of a validation method call: |
| 120 | + |
| 121 | +```python |
| 122 | +pb.Step("col_vals_gt", columns="amount", value=0) |
| 123 | +pb.Step("col_vals_between", columns="score", left=0, right=100) |
| 124 | +pb.Step("col_vals_in_set", columns="status", set=["a", "b", "c"]) |
| 125 | +pb.Step("col_vals_not_null", columns="id") |
| 126 | +pb.Step("rows_distinct", columns_subset=["id"]) |
| 127 | +pb.Step("col_schema_match", schema=my_schema, complete=True) |
| 128 | +pb.Step("row_count_match", count=1000, tol=50) |
| 129 | +``` |
| 130 | + |
| 131 | +The `method` argument is any Validate method name. All remaining |
| 132 | +keyword arguments are passed to that method. |
| 133 | + |
| 134 | +### Schema |
| 135 | + |
| 136 | +Define expected table structure: |
| 137 | + |
| 138 | +```python |
| 139 | +# From keyword arguments |
| 140 | +schema = pb.Schema(id="Int64", name="String", age="Int32") |
| 141 | + |
| 142 | +# From a dictionary |
| 143 | +schema = pb.Schema({"id": "Int64", "name": "String"}) |
| 144 | + |
| 145 | +# From a list of tuples |
| 146 | +schema = pb.Schema([("id", "Int64"), ("name", "String")]) |
| 147 | + |
| 148 | +# Column names only (no type checking) |
| 149 | +schema = pb.Schema(["id", "name", "age"]) |
| 150 | + |
| 151 | +# Infer from an existing table |
| 152 | +schema = pb.schema_from_tbl(df) |
| 153 | +schema = pb.Schema.from_table(df, infer_constraints=True) |
| 154 | +``` |
| 155 | + |
| 156 | +### Validating against a contract |
| 157 | + |
| 158 | +```python |
| 159 | +# Returns a Validate object (already interrogated) |
| 160 | +validation = contract.validate(data=df) |
| 161 | + |
| 162 | +# Check results |
| 163 | +validation.all_passed() |
| 164 | +validation.get_tabular_report() |
| 165 | +``` |
| 166 | + |
| 167 | +Or convert to a Validate object for further customization: |
| 168 | + |
| 169 | +```python |
| 170 | +v = contract.to_validate(data=df) |
| 171 | +# Add more steps if needed |
| 172 | +v = v.col_vals_gt(columns="extra_col", value=0) |
| 173 | +v = v.interrogate() |
| 174 | +``` |
| 175 | + |
| 176 | +### on_violation behavior |
| 177 | + |
| 178 | +| Value | Behavior | |
| 179 | +| --------- | ------------------------------------ | |
| 180 | +| `"warn"` | Print a warning message (default) | |
| 181 | +| `"raise"` | Raise an exception if any step fails | |
| 182 | +| `"log"` | Log the violation silently | |
| 183 | + |
| 184 | +### Pipeline |
| 185 | + |
| 186 | +A `Pipeline` orchestrates source and target contract validation |
| 187 | +around a data transformation: |
| 188 | + |
| 189 | +```python |
| 190 | +source_contract = pb.Contract( |
| 191 | + name="raw-orders", |
| 192 | + direction="source", |
| 193 | + schema=pb.Schema(id="Int64", amount="Float64"), |
| 194 | + steps=[pb.Step("col_vals_not_null", columns="id")], |
| 195 | + on_violation="raise", |
| 196 | +) |
| 197 | + |
| 198 | +target_contract = pb.Contract( |
| 199 | + name="clean-orders", |
| 200 | + direction="target", |
| 201 | + schema=pb.Schema(id="Int64", amount="Float64", is_valid="Boolean"), |
| 202 | + steps=[ |
| 203 | + pb.Step("col_vals_gt", columns="amount", value=0), |
| 204 | + pb.Step("col_vals_not_null", columns="is_valid"), |
| 205 | + ], |
| 206 | + on_violation="warn", |
| 207 | +) |
| 208 | + |
| 209 | +pipeline = pb.Pipeline( |
| 210 | + source=source_contract, |
| 211 | + target=target_contract, |
| 212 | + label="Order cleaning pipeline", |
| 213 | + short_circuit=True, # skip target if source fails |
| 214 | +) |
| 215 | + |
| 216 | +def transform(df): |
| 217 | + return df.with_columns(is_valid=pl.col("amount") > 0) |
| 218 | + |
| 219 | +result = pipeline.run(data=raw_df, transform=transform) |
| 220 | +``` |
| 221 | + |
| 222 | +### PipelineResult |
| 223 | + |
| 224 | +```python |
| 225 | +result.passed # True if both source and target passed |
| 226 | +result.source_passed # True if source contract passed |
| 227 | +result.target_passed # True if target contract passed |
| 228 | +result.source_validation # Validate object for source |
| 229 | +result.target_validation # Validate object for target |
| 230 | +result.transform_output # the transformed data |
| 231 | +result.get_report() # summary report string |
| 232 | +``` |
| 233 | + |
| 234 | +### Serialization |
| 235 | + |
| 236 | +```python |
| 237 | +# Save contract to YAML |
| 238 | +contract.to_yaml("contracts/orders-source.yaml") |
| 239 | + |
| 240 | +# Load contract from YAML |
| 241 | +contract = pb.Contract.from_yaml("contracts/orders-source.yaml") |
| 242 | + |
| 243 | +# Dictionary round-trip |
| 244 | +d = contract.to_dict() |
| 245 | +contract = pb.Contract.from_dict(d) |
| 246 | + |
| 247 | +# Pipeline serialization |
| 248 | +pipeline.to_yaml("pipelines/order-cleaning.yaml") |
| 249 | +pipeline = pb.Pipeline.from_yaml("pipelines/order-cleaning.yaml") |
| 250 | +``` |
| 251 | + |
| 252 | +## Workflows |
| 253 | + |
| 254 | +### Setting up a new contract |
| 255 | + |
| 256 | +1. Profile the data with `pb.DataScan(data=df)` to understand its |
| 257 | + shape, types, and distributions. |
| 258 | +2. Infer a starting schema: `schema = pb.schema_from_tbl(df)`. |
| 259 | +3. Define steps for the semantic rules your domain requires. |
| 260 | +4. Choose `on_violation` based on criticality. |
| 261 | +5. Test with `contract.validate(data=df)`. |
| 262 | +6. Serialize to YAML for version control. |
| 263 | + |
| 264 | +### Adding contracts to an existing pipeline |
| 265 | + |
| 266 | +1. Define source and target contracts. |
| 267 | +2. Wrap the transformation in a `Pipeline`. |
| 268 | +3. Use `short_circuit=True` to skip the transform when source |
| 269 | + validation fails. |
| 270 | +4. Check `result.passed` to gate downstream processing. |
| 271 | + |
| 272 | +### Evolving contracts over time |
| 273 | + |
| 274 | +When schema or rules change: |
| 275 | + |
| 276 | +1. Update the schema and steps in the contract YAML. |
| 277 | +2. Bump the `version` field. |
| 278 | +3. Test against representative data. |
| 279 | +4. Communicate changes to `consumers`. |
| 280 | + |
| 281 | +## Gotchas |
| 282 | + |
| 283 | +1. **`direction` is metadata, not enforcement.** It documents intent |
| 284 | + but doesn't change validation behavior. |
| 285 | +2. **`on_violation="raise"` stops execution.** Use `"warn"` or |
| 286 | + `"log"` when you want to continue despite failures. |
| 287 | +3. **`short_circuit=True` skips target validation** if source |
| 288 | + validation fails. Set to `False` to always run both. |
| 289 | +4. **Schema type strings are backend-specific.** Use the dtype names |
| 290 | + from your backend (e.g., `"Int64"` for Polars, `"int64"` for |
| 291 | + Pandas). |
| 292 | +5. **`to_validate()` does not call `interrogate()`.** Call it yourself |
| 293 | + if you add steps. Use `validate()` for automatic interrogation. |
| 294 | +6. **Steps reference method names as strings.** Typos in method names |
| 295 | + surface at validation time, not at contract creation. |
0 commit comments