lazarillo 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. lazarillo-0.1.0/.github/workflows/ci.yml +41 -0
  2. lazarillo-0.1.0/.github/workflows/release.yml +63 -0
  3. lazarillo-0.1.0/.gitignore +15 -0
  4. lazarillo-0.1.0/CHANGELOG.md +16 -0
  5. lazarillo-0.1.0/LICENSE +21 -0
  6. lazarillo-0.1.0/PKG-INFO +219 -0
  7. lazarillo-0.1.0/README.md +157 -0
  8. lazarillo-0.1.0/docs/aws.md +101 -0
  9. lazarillo-0.1.0/docs/checkride.md +26 -0
  10. lazarillo-0.1.0/docs/dbt-cloud.md +39 -0
  11. lazarillo-0.1.0/docs/redshift.md +108 -0
  12. lazarillo-0.1.0/docs/releasing.md +26 -0
  13. lazarillo-0.1.0/examples/tarima-tickets/README.md +16 -0
  14. lazarillo-0.1.0/examples/tarima-tickets/build_demo.py +189 -0
  15. lazarillo-0.1.0/examples/tarima-tickets/lazarillo.yml +23 -0
  16. lazarillo-0.1.0/examples/tarima-tickets/tarima/dbt_project.yml +13 -0
  17. lazarillo-0.1.0/examples/tarima-tickets/tarima/macros/generate_schema_name.sql +4 -0
  18. lazarillo-0.1.0/examples/tarima-tickets/tarima/models/marts/_marts.yml +37 -0
  19. lazarillo-0.1.0/examples/tarima-tickets/tarima/models/marts/fct_event_revenue.sql +9 -0
  20. lazarillo-0.1.0/examples/tarima-tickets/tarima/models/marts/fct_orders.sql +25 -0
  21. lazarillo-0.1.0/examples/tarima-tickets/tarima/models/staging/_sources.yml +9 -0
  22. lazarillo-0.1.0/examples/tarima-tickets/tarima/models/staging/stg_events.sql +8 -0
  23. lazarillo-0.1.0/examples/tarima-tickets/tarima/models/staging/stg_orders.sql +11 -0
  24. lazarillo-0.1.0/examples/tarima-tickets/tarima/profiles.yml +14 -0
  25. lazarillo-0.1.0/pyproject.toml +66 -0
  26. lazarillo-0.1.0/src/lazarillo/__init__.py +3 -0
  27. lazarillo-0.1.0/src/lazarillo/cli.py +173 -0
  28. lazarillo-0.1.0/src/lazarillo/config.py +206 -0
  29. lazarillo-0.1.0/src/lazarillo/context.py +171 -0
  30. lazarillo-0.1.0/src/lazarillo/dbt_cloud.py +91 -0
  31. lazarillo-0.1.0/src/lazarillo/diff.py +140 -0
  32. lazarillo-0.1.0/src/lazarillo/engine.py +38 -0
  33. lazarillo-0.1.0/src/lazarillo/guardrails.py +55 -0
  34. lazarillo-0.1.0/src/lazarillo/init.py +110 -0
  35. lazarillo-0.1.0/src/lazarillo/lake.py +207 -0
  36. lazarillo-0.1.0/src/lazarillo/mcp_server.py +85 -0
  37. lazarillo-0.1.0/src/lazarillo/redshift.py +168 -0
  38. lazarillo-0.1.0/src/lazarillo/verify.py +94 -0
  39. lazarillo-0.1.0/src/lazarillo/warehouse.py +192 -0
  40. lazarillo-0.1.0/tests/conftest.py +17 -0
  41. lazarillo-0.1.0/tests/test_context.py +48 -0
  42. lazarillo-0.1.0/tests/test_dbt_cloud.py +179 -0
  43. lazarillo-0.1.0/tests/test_diff.py +25 -0
  44. lazarillo-0.1.0/tests/test_ducklake.py +59 -0
  45. lazarillo-0.1.0/tests/test_guardrails.py +53 -0
  46. lazarillo-0.1.0/tests/test_init.py +32 -0
  47. lazarillo-0.1.0/tests/test_lake.py +173 -0
  48. lazarillo-0.1.0/tests/test_redshift.py +298 -0
  49. lazarillo-0.1.0/tests/test_s3.py +286 -0
@@ -0,0 +1,41 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python: ["3.11", "3.12", "3.13"]
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: ${{ matrix.python }}
23
+ cache: pip
24
+ - run: pip install -e ".[dev]"
25
+ - run: pytest -q
26
+
27
+ package:
28
+ runs-on: ubuntu-latest
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.12"
34
+ - run: pip install build twine
35
+ - run: python -m build
36
+ - run: twine check --strict dist/*
37
+ - name: Install the wheel in a clean venv
38
+ run: |
39
+ python -m venv /tmp/clean
40
+ /tmp/clean/bin/pip install dist/*.whl
41
+ /tmp/clean/bin/lazarillo --version
@@ -0,0 +1,63 @@
1
+ name: Release
2
+
3
+ # Push a tag like v0.1.0: builds once, publishes to TestPyPI, then to PyPI.
4
+ # Uses PyPI Trusted Publishing, so no tokens are stored. Setup in docs/releasing.md.
5
+
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.12"
21
+ - run: pip install build twine
22
+ - name: Check the tag matches the package version
23
+ run: |
24
+ version=$(python -c "import re; print(re.search(r'__version__ = \"(.+)\"', open('src/lazarillo/__init__.py').read())[1])")
25
+ test "v$version" = "$GITHUB_REF_NAME" || { echo "tag $GITHUB_REF_NAME != v$version"; exit 1; }
26
+ - run: python -m build
27
+ - run: twine check --strict dist/*
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+
33
+ testpypi:
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment:
37
+ name: testpypi
38
+ url: https://test.pypi.org/p/lazarillo
39
+ permissions:
40
+ id-token: write
41
+ steps:
42
+ - uses: actions/download-artifact@v4
43
+ with:
44
+ name: dist
45
+ path: dist/
46
+ - uses: pypa/gh-action-pypi-publish@release/v1
47
+ with:
48
+ repository-url: https://test.pypi.org/legacy/
49
+
50
+ pypi:
51
+ needs: testpypi
52
+ runs-on: ubuntu-latest
53
+ environment:
54
+ name: pypi
55
+ url: https://pypi.org/p/lazarillo
56
+ permissions:
57
+ id-token: write
58
+ steps:
59
+ - uses: actions/download-artifact@v4
60
+ with:
61
+ name: dist
62
+ path: dist/
63
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+ .lazarillo/
9
+ # example artefacts (regenerated by each example's build script)
10
+ examples/*/data/
11
+ examples/*/*/target/
12
+ examples/*/*/target_dev/
13
+ examples/*/*/logs/
14
+ examples/*/*/dbt_packages/
15
+ .user.yml
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First public release.
6
+
7
+ - `lazarillo map`, `describe` and `impact`: a data map from the dbt manifest, local or from a
8
+ dbt Cloud job, with materializations, incremental strategies, tests, lineage and exposures.
9
+ - `lazarillo query`: one read-only statement per call, row-capped, with PII masking and no
10
+ filesystem access.
11
+ - `lazarillo diff`: row-level diff between warehouse tables and lake files (Delta, Parquet,
12
+ Iceberg), locally or on S3, AWS Glue and DuckLake.
13
+ - `lazarillo verify`: builds a model in dev, diffs it against prod and reports the blast radius.
14
+ - Warehouses: DuckDB and Redshift (password or IAM, read-only session).
15
+ - `lazarillo init` and `lazarillo mcp`: the same tools over MCP, returning the same Markdown.
16
+ - Tarima Tickets demo in `examples/`, with three planted data problems.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guillermo Vizcaíno Román
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.5
2
+ Name: lazarillo
3
+ Version: 0.1.0
4
+ Summary: A data harness that guides AI agents through your lakehouse: context, guardrails and verification.
5
+ Project-URL: Homepage, https://github.com/guille-vizcaino/lazarillo
6
+ Project-URL: Documentation, https://github.com/guille-vizcaino/lazarillo#readme
7
+ Project-URL: Issues, https://github.com/guille-vizcaino/lazarillo/issues
8
+ Author-email: Guillermo Vizcaíno Román <hi@guille.me>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agents,data-diff,data-engineering,dbt,delta-lake,harness,iceberg,mcp,redshift
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Database
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: duckdb>=1.1
24
+ Requires-Dist: pyarrow>=16
25
+ Requires-Dist: pyyaml>=6
26
+ Provides-Extra: all
27
+ Requires-Dist: boto3>=1.34; extra == 'all'
28
+ Requires-Dist: dbt-duckdb>=1.8; extra == 'all'
29
+ Requires-Dist: deltalake>=0.20; extra == 'all'
30
+ Requires-Dist: mcp>=2; extra == 'all'
31
+ Requires-Dist: psycopg2-binary>=2.9; extra == 'all'
32
+ Requires-Dist: pyiceberg[glue]>=0.8; extra == 'all'
33
+ Requires-Dist: pyiceberg[pyarrow,sql-sqlite]>=0.8; extra == 'all'
34
+ Provides-Extra: dbt
35
+ Requires-Dist: dbt-duckdb>=1.8; extra == 'dbt'
36
+ Provides-Extra: delta
37
+ Requires-Dist: deltalake>=0.20; extra == 'delta'
38
+ Provides-Extra: dev
39
+ Requires-Dist: boto3>=1.34; extra == 'dev'
40
+ Requires-Dist: dbt-duckdb>=1.8; extra == 'dev'
41
+ Requires-Dist: deltalake>=0.20; extra == 'dev'
42
+ Requires-Dist: mcp>=2; extra == 'dev'
43
+ Requires-Dist: moto[server]>=5; extra == 'dev'
44
+ Requires-Dist: psycopg2-binary>=2.9; extra == 'dev'
45
+ Requires-Dist: pyiceberg[glue]>=0.8; extra == 'dev'
46
+ Requires-Dist: pyiceberg[pyarrow,sql-sqlite]>=0.8; extra == 'dev'
47
+ Requires-Dist: pytest>=8; extra == 'dev'
48
+ Provides-Extra: glue
49
+ Requires-Dist: boto3>=1.34; extra == 'glue'
50
+ Requires-Dist: pyiceberg[glue]>=0.8; extra == 'glue'
51
+ Requires-Dist: pyiceberg[pyarrow,sql-sqlite]>=0.8; extra == 'glue'
52
+ Provides-Extra: iceberg
53
+ Requires-Dist: pyiceberg[pyarrow,sql-sqlite]>=0.8; extra == 'iceberg'
54
+ Provides-Extra: mcp
55
+ Requires-Dist: mcp>=2; extra == 'mcp'
56
+ Provides-Extra: redshift
57
+ Requires-Dist: boto3>=1.34; extra == 'redshift'
58
+ Requires-Dist: psycopg2-binary>=2.9; extra == 'redshift'
59
+ Provides-Extra: s3
60
+ Requires-Dist: boto3>=1.34; extra == 's3'
61
+ Description-Content-Type: text/markdown
62
+
63
+ # Lazarillo
64
+
65
+ > **Lazarillo** (n.): the boy who guides a blind man across 16th-century Spain in
66
+ > *Lazarillo de Tormes*. Your AI agent can't see your warehouse. Lazarillo guides it.
67
+
68
+ Lazarillo is a **data harness**: the layer between an AI agent and your lakehouse that
69
+ gives the agent **context**, enforces **guardrails** and **verifies** every change before it
70
+ reaches production.
71
+
72
+ SaaS products used to wrap a database in a UI. The equivalent today is a harness: it wraps
73
+ your data, your conventions and your workflows so that an agent can work on them
74
+ safely. Lazarillo is an opinionated harness for analytics engineering. It is local-first,
75
+ vendor-neutral, and exposed as a CLI and an MCP server.
76
+
77
+ ## Why
78
+
79
+ Agents already write decent SQL. They fail in three other ways:
80
+
81
+ | The agent… | Lazarillo gives it… |
82
+ |---|---|
83
+ | doesn't know what `fct_orders` means, how it's built, or who reads it | **Context**: a data map compiled from the dbt manifest, covering materializations, incremental strategies, tests, lineage and exposures such as Power BI dashboards. |
84
+ | can `DROP` a table, scan 3 TB or print customer emails | **Guardrails** in code, not in the prompt: read-only, one statement per call, row caps, PII masking, no filesystem access. |
85
+ | says "done ✅" without proof | **Verification**: build the change in a dev schema, diff it row by row against prod, and show the blast radius. |
86
+
87
+ The picaresque twist: Lázaro famously tricks the blind man. So the harness **never trusts
88
+ the agent's word**. Every claim comes with a diff.
89
+
90
+ ## Use it on your project
91
+
92
+ ```bash
93
+ pip install "lazarillo[all]"
94
+ cd your-project
95
+ lazarillo init # writes lazarillo.yml and finds your dbt project, if any
96
+ lazarillo query "select 42"
97
+ ```
98
+
99
+ `lazarillo init` prints the `.mcp.json` block that hands the harness to your agent. The core
100
+ only needs DuckDB; pick extras for the rest of your stack: `delta`, `iceberg`, `s3`, `glue`,
101
+ `redshift`, `dbt`, `mcp`, or `all`. Without dbt you still get `query` and `diff`; add a dbt project to unlock `map`,
102
+ `describe`, `impact` and `verify`. If production runs in dbt Cloud, the map can come from
103
+ your production job instead ([docs/dbt-cloud.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/dbt-cloud.md)).
104
+
105
+ ## Try the demo (2 minutes, no cloud account)
106
+
107
+ ```bash
108
+ git clone https://github.com/guille-vizcaino/lazarillo && cd lazarillo
109
+ python -m venv .venv && source .venv/bin/activate
110
+ pip install -e ".[all]"
111
+ python examples/tarima-tickets/build_demo.py # builds Tarima Tickets, a fictional concert ticketing company
112
+ cd examples/tarima-tickets && lazarillo map
113
+ ```
114
+
115
+ The demo lives in [`examples/tarima-tickets`](https://github.com/guille-vizcaino/lazarillo/tree/main/examples/tarima-tickets) and uses Lazarillo
116
+ exactly like your project would: its own `lazarillo.yml`, dbt project and data. It mirrors a common AWS setup (ticketing system → S3 landing in Delta, migrating
117
+ to Iceberg → warehouse → dbt → Power BI) on DuckDB. Three problems are planted in it,
118
+ the kind that reach production every week:
119
+
120
+ ### 1. Source vs landing: "are we extracting everything?"
121
+
122
+ ```console
123
+ $ lazarillo diff src.orders landing.orders -k order_id
124
+ | rows | 3,996 | 3,961 |
125
+ | only here | 35 | 0 |
126
+ Schema drift
127
+ - `sales_channel` only in left
128
+ ```
129
+
130
+ The extract uses an `updated_at` watermark. Box office sales reach the ticketing system
131
+ days later, when the venue syncs, so they fall behind the watermark and are never landed.
132
+ The source also grew a column the extract ignores.
133
+
134
+ ### 2. Delta → Iceberg migration: "is the new table identical?"
135
+
136
+ ```console
137
+ $ lazarillo diff delta:data/landing/delta/orders iceberg:landing.orders -k order_id
138
+ | changed (same key) | 1,081 |
139
+ - `ordered_at`: 1,081 rows
140
+ | 2 | 2026-09-11 05:47:14 | 2026-09-11 03:47:14 |
141
+ ```
142
+
143
+ Same row count and same keys, so a count-based check passes. The new writer stores
144
+ timestamps in UTC, and a row-level diff catches the shift.
145
+
146
+ ### 3. Incremental drift: "is prod what a full rebuild would give?"
147
+
148
+ ```console
149
+ $ lazarillo verify fct_orders
150
+ Built `dev.fct_orders` and compared it with `analytics.fct_orders`.
151
+ - `status`: 38 rows (paid → refunded)
152
+ ### Blast radius
153
+ - downstream models: fct_event_revenue
154
+ - **Revenue by event** (dashboard, owner: Finance)
155
+ ⚠️ Dev and prod differ. Review the diff above before merging; 1 exposure(s) will see the change.
156
+ ```
157
+
158
+ The incremental filter only picks up *new* orders, so later refunds never reach
159
+ production. The revenue-by-event dashboard overstates what each promoter is owed.
160
+ `verify` is also the loop an agent uses after editing a model: change the filter to
161
+ `updated_at`, verify, and show the diff.
162
+
163
+ ## Use it with an agent
164
+
165
+ ```bash
166
+ lazarillo -c examples/tarima-tickets/lazarillo.yml mcp # MCP over stdio
167
+ ```
168
+
169
+ This repo ships a `.mcp.json` wired to the demo, so opening it in Claude Code gives the agent these tools:
170
+ `data_map`, `describe_model`, `impact`, `query`, `diff` and `verify`. The server also sends
171
+ instructions that tell the agent to verify before it claims success.
172
+
173
+ Try: *"Finance says the payout for Los Tejados in Barcelona looks too high. Find out why and propose a fix."*
174
+
175
+ ## Commands
176
+
177
+ | command | what it does |
178
+ |---|---|
179
+ | `lazarillo init [DIR]` | Write a starter `lazarillo.yml` and print the MCP config |
180
+ | `lazarillo map` | Sources, models, materializations, exposures |
181
+ | `lazarillo describe MODEL` | Columns, tests, SQL, upstream and downstream |
182
+ | `lazarillo impact MODEL` | Downstream models and dashboards |
183
+ | `lazarillo query "SQL"` | One read-only statement, capped and masked |
184
+ | `lazarillo diff LEFT RIGHT -k KEY [--where]` | Row-level diff between any two relations |
185
+ | `lazarillo verify MODEL [--where]` | Build in dev, diff against prod, report blast radius |
186
+ | `lazarillo mcp` | Serve all of the above over MCP |
187
+
188
+ Relations can be `schema.table`, `attached_db.table`, `delta:<path>`,
189
+ `iceberg:<namespace.table>` or `parquet:<glob>`. Paths can be local or `s3://`, Iceberg
190
+ catalogs can be SQL, AWS Glue or REST, and DuckLakes can be attached next to the
191
+ warehouse. See [docs/aws.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/aws.md). The warehouse itself can be a DuckDB file or
192
+ Redshift, with IAM auth; see [docs/redshift.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/redshift.md).
193
+
194
+ ## Scope
195
+
196
+ **In scope:** everything from landing to consumption, i.e. landing tables, dbt models and
197
+ the dashboards that read them.
198
+
199
+ **Out of scope:** orchestration and scheduling (Step Functions, Airflow, EventBridge…).
200
+ Lazarillo doesn't run your pipelines. It checks what they produce.
201
+
202
+ ## Roadmap
203
+
204
+ - [x] Data map from the dbt manifest, including exposures
205
+ - [x] Read-only guardrails, row caps and PII masking
206
+ - [x] Row-level diff across warehouse, Delta, Iceberg and Parquet
207
+ - [x] `verify`: dev build, diff and blast radius
208
+ - [x] MCP server
209
+ - [x] Lake in S3 (Delta, Parquet, Iceberg), AWS Glue catalog and DuckLake
210
+ - [x] Redshift as the warehouse, with IAM auth ([docs/redshift.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/redshift.md))
211
+ - [x] dbt Cloud: read the manifest of the production job
212
+ - [ ] `--defer` builds so `verify` doesn't rebuild parents
213
+ - [ ] Cost guardrails (`EXPLAIN`-based scan budget)
214
+ - [ ] **`lazarillo checkride`**: a benchmark of real data-engineering tasks, scored with
215
+ and without the harness ([docs/checkride.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/checkride.md))
216
+
217
+ ## License
218
+
219
+ MIT © Guillermo Vizcaíno Román. The demo company and all its data are fictional.
@@ -0,0 +1,157 @@
1
+ # Lazarillo
2
+
3
+ > **Lazarillo** (n.): the boy who guides a blind man across 16th-century Spain in
4
+ > *Lazarillo de Tormes*. Your AI agent can't see your warehouse. Lazarillo guides it.
5
+
6
+ Lazarillo is a **data harness**: the layer between an AI agent and your lakehouse that
7
+ gives the agent **context**, enforces **guardrails** and **verifies** every change before it
8
+ reaches production.
9
+
10
+ SaaS products used to wrap a database in a UI. The equivalent today is a harness: it wraps
11
+ your data, your conventions and your workflows so that an agent can work on them
12
+ safely. Lazarillo is an opinionated harness for analytics engineering. It is local-first,
13
+ vendor-neutral, and exposed as a CLI and an MCP server.
14
+
15
+ ## Why
16
+
17
+ Agents already write decent SQL. They fail in three other ways:
18
+
19
+ | The agent… | Lazarillo gives it… |
20
+ |---|---|
21
+ | doesn't know what `fct_orders` means, how it's built, or who reads it | **Context**: a data map compiled from the dbt manifest, covering materializations, incremental strategies, tests, lineage and exposures such as Power BI dashboards. |
22
+ | can `DROP` a table, scan 3 TB or print customer emails | **Guardrails** in code, not in the prompt: read-only, one statement per call, row caps, PII masking, no filesystem access. |
23
+ | says "done ✅" without proof | **Verification**: build the change in a dev schema, diff it row by row against prod, and show the blast radius. |
24
+
25
+ The picaresque twist: Lázaro famously tricks the blind man. So the harness **never trusts
26
+ the agent's word**. Every claim comes with a diff.
27
+
28
+ ## Use it on your project
29
+
30
+ ```bash
31
+ pip install "lazarillo[all]"
32
+ cd your-project
33
+ lazarillo init # writes lazarillo.yml and finds your dbt project, if any
34
+ lazarillo query "select 42"
35
+ ```
36
+
37
+ `lazarillo init` prints the `.mcp.json` block that hands the harness to your agent. The core
38
+ only needs DuckDB; pick extras for the rest of your stack: `delta`, `iceberg`, `s3`, `glue`,
39
+ `redshift`, `dbt`, `mcp`, or `all`. Without dbt you still get `query` and `diff`; add a dbt project to unlock `map`,
40
+ `describe`, `impact` and `verify`. If production runs in dbt Cloud, the map can come from
41
+ your production job instead ([docs/dbt-cloud.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/dbt-cloud.md)).
42
+
43
+ ## Try the demo (2 minutes, no cloud account)
44
+
45
+ ```bash
46
+ git clone https://github.com/guille-vizcaino/lazarillo && cd lazarillo
47
+ python -m venv .venv && source .venv/bin/activate
48
+ pip install -e ".[all]"
49
+ python examples/tarima-tickets/build_demo.py # builds Tarima Tickets, a fictional concert ticketing company
50
+ cd examples/tarima-tickets && lazarillo map
51
+ ```
52
+
53
+ The demo lives in [`examples/tarima-tickets`](https://github.com/guille-vizcaino/lazarillo/tree/main/examples/tarima-tickets) and uses Lazarillo
54
+ exactly like your project would: its own `lazarillo.yml`, dbt project and data. It mirrors a common AWS setup (ticketing system → S3 landing in Delta, migrating
55
+ to Iceberg → warehouse → dbt → Power BI) on DuckDB. Three problems are planted in it,
56
+ the kind that reach production every week:
57
+
58
+ ### 1. Source vs landing: "are we extracting everything?"
59
+
60
+ ```console
61
+ $ lazarillo diff src.orders landing.orders -k order_id
62
+ | rows | 3,996 | 3,961 |
63
+ | only here | 35 | 0 |
64
+ Schema drift
65
+ - `sales_channel` only in left
66
+ ```
67
+
68
+ The extract uses an `updated_at` watermark. Box office sales reach the ticketing system
69
+ days later, when the venue syncs, so they fall behind the watermark and are never landed.
70
+ The source also grew a column the extract ignores.
71
+
72
+ ### 2. Delta → Iceberg migration: "is the new table identical?"
73
+
74
+ ```console
75
+ $ lazarillo diff delta:data/landing/delta/orders iceberg:landing.orders -k order_id
76
+ | changed (same key) | 1,081 |
77
+ - `ordered_at`: 1,081 rows
78
+ | 2 | 2026-09-11 05:47:14 | 2026-09-11 03:47:14 |
79
+ ```
80
+
81
+ Same row count and same keys, so a count-based check passes. The new writer stores
82
+ timestamps in UTC, and a row-level diff catches the shift.
83
+
84
+ ### 3. Incremental drift: "is prod what a full rebuild would give?"
85
+
86
+ ```console
87
+ $ lazarillo verify fct_orders
88
+ Built `dev.fct_orders` and compared it with `analytics.fct_orders`.
89
+ - `status`: 38 rows (paid → refunded)
90
+ ### Blast radius
91
+ - downstream models: fct_event_revenue
92
+ - **Revenue by event** (dashboard, owner: Finance)
93
+ ⚠️ Dev and prod differ. Review the diff above before merging; 1 exposure(s) will see the change.
94
+ ```
95
+
96
+ The incremental filter only picks up *new* orders, so later refunds never reach
97
+ production. The revenue-by-event dashboard overstates what each promoter is owed.
98
+ `verify` is also the loop an agent uses after editing a model: change the filter to
99
+ `updated_at`, verify, and show the diff.
100
+
101
+ ## Use it with an agent
102
+
103
+ ```bash
104
+ lazarillo -c examples/tarima-tickets/lazarillo.yml mcp # MCP over stdio
105
+ ```
106
+
107
+ This repo ships a `.mcp.json` wired to the demo, so opening it in Claude Code gives the agent these tools:
108
+ `data_map`, `describe_model`, `impact`, `query`, `diff` and `verify`. The server also sends
109
+ instructions that tell the agent to verify before it claims success.
110
+
111
+ Try: *"Finance says the payout for Los Tejados in Barcelona looks too high. Find out why and propose a fix."*
112
+
113
+ ## Commands
114
+
115
+ | command | what it does |
116
+ |---|---|
117
+ | `lazarillo init [DIR]` | Write a starter `lazarillo.yml` and print the MCP config |
118
+ | `lazarillo map` | Sources, models, materializations, exposures |
119
+ | `lazarillo describe MODEL` | Columns, tests, SQL, upstream and downstream |
120
+ | `lazarillo impact MODEL` | Downstream models and dashboards |
121
+ | `lazarillo query "SQL"` | One read-only statement, capped and masked |
122
+ | `lazarillo diff LEFT RIGHT -k KEY [--where]` | Row-level diff between any two relations |
123
+ | `lazarillo verify MODEL [--where]` | Build in dev, diff against prod, report blast radius |
124
+ | `lazarillo mcp` | Serve all of the above over MCP |
125
+
126
+ Relations can be `schema.table`, `attached_db.table`, `delta:<path>`,
127
+ `iceberg:<namespace.table>` or `parquet:<glob>`. Paths can be local or `s3://`, Iceberg
128
+ catalogs can be SQL, AWS Glue or REST, and DuckLakes can be attached next to the
129
+ warehouse. See [docs/aws.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/aws.md). The warehouse itself can be a DuckDB file or
130
+ Redshift, with IAM auth; see [docs/redshift.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/redshift.md).
131
+
132
+ ## Scope
133
+
134
+ **In scope:** everything from landing to consumption, i.e. landing tables, dbt models and
135
+ the dashboards that read them.
136
+
137
+ **Out of scope:** orchestration and scheduling (Step Functions, Airflow, EventBridge…).
138
+ Lazarillo doesn't run your pipelines. It checks what they produce.
139
+
140
+ ## Roadmap
141
+
142
+ - [x] Data map from the dbt manifest, including exposures
143
+ - [x] Read-only guardrails, row caps and PII masking
144
+ - [x] Row-level diff across warehouse, Delta, Iceberg and Parquet
145
+ - [x] `verify`: dev build, diff and blast radius
146
+ - [x] MCP server
147
+ - [x] Lake in S3 (Delta, Parquet, Iceberg), AWS Glue catalog and DuckLake
148
+ - [x] Redshift as the warehouse, with IAM auth ([docs/redshift.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/redshift.md))
149
+ - [x] dbt Cloud: read the manifest of the production job
150
+ - [ ] `--defer` builds so `verify` doesn't rebuild parents
151
+ - [ ] Cost guardrails (`EXPLAIN`-based scan budget)
152
+ - [ ] **`lazarillo checkride`**: a benchmark of real data-engineering tasks, scored with
153
+ and without the harness ([docs/checkride.md](https://github.com/guille-vizcaino/lazarillo/blob/main/docs/checkride.md))
154
+
155
+ ## License
156
+
157
+ MIT © Guillermo Vizcaíno Román. The demo company and all its data are fictional.
@@ -0,0 +1,101 @@
1
+ # Reading a lake in S3, Glue and DuckLake
2
+
3
+ Lazarillo reads lake tables from local disk or from S3. Iceberg tables come from any
4
+ catalog pyiceberg supports (SQL, AWS Glue, REST...), and DuckLakes can be attached next
5
+ to the warehouse. None of this needs a cloud account to try; see *Testing without AWS*.
6
+
7
+ ```bash
8
+ pip install "lazarillo[delta,glue]" # glue brings iceberg and boto3
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ```yaml
14
+ warehouse:
15
+ path: warehouse.duckdb
16
+
17
+ attach:
18
+ src: source.duckdb # another DuckDB file, as before
19
+ lake: ducklake:lake/metadata.ducklake # a DuckLake (files local or in S3)
20
+ # lake: {type: ducklake, path: "postgres:dbname=lake", data_path: s3://my-lake/ducklake/}
21
+
22
+ landing:
23
+ # Where `delta:` and `parquet:` refs may read. Anything else is refused.
24
+ # Defaults to the folder holding lazarillo.yml.
25
+ locations:
26
+ - s3://my-lake/landing/
27
+ s3: # every key is optional
28
+ region: eu-west-1
29
+ profile: lake-readonly # or leave it out and use the AWS default chain
30
+ # endpoint: http://localhost:9000 # MinIO, LocalStack...
31
+ iceberg_catalog: # passed to pyiceberg's load_catalog
32
+ name: glue
33
+ type: glue
34
+ # glue.id: "123456789012" # another account's catalog
35
+ ```
36
+
37
+ Then refer to tables as usual:
38
+
39
+ ```console
40
+ $ lazarillo diff delta:s3://my-lake/landing/delta/orders iceberg:landing.orders -k order_id
41
+ $ lazarillo diff parquet:s3://my-lake/landing/raw/dt=2026-09-*/*.parquet landing.orders -k order_id
42
+ $ lazarillo query "select status, count(*) from lake.orders group by 1"
43
+ ```
44
+
45
+ ## Credentials
46
+
47
+ Lazarillo resolves credentials once with boto3 and hands the same identity to every
48
+ reader (deltalake, pyarrow, pyiceberg and DuckDB for DuckLake), so SSO and assume-role
49
+ profiles work everywhere. The order is:
50
+
51
+ 1. `access_key_id` / `secret_access_key` in `landing.s3` (avoid committing them).
52
+ 2. `profile` in `landing.s3`.
53
+ 3. The AWS default chain: environment variables, `AWS_PROFILE`, ~/.aws, instance or task role.
54
+
55
+ Keys in `landing.s3` also fill in the Glue catalog's credentials and region. Anything
56
+ set on `iceberg_catalog` itself (`glue.region`, `s3.endpoint`...) wins.
57
+
58
+ Lazarillo only reads. A policy like this one is enough:
59
+
60
+ ```json
61
+ {
62
+ "Version": "2012-10-17",
63
+ "Statement": [
64
+ {"Effect": "Allow", "Action": ["s3:GetObject", "s3:ListBucket"],
65
+ "Resource": ["arn:aws:s3:::my-lake", "arn:aws:s3:::my-lake/landing/*"]},
66
+ {"Effect": "Allow", "Action": ["glue:GetDatabase", "glue:GetTable", "glue:GetTables"],
67
+ "Resource": "*"}
68
+ ]
69
+ }
70
+ ```
71
+
72
+ ## Guardrails
73
+
74
+ - SQL still cannot touch files or the network (`enable_external_access = false`).
75
+ Delta, Parquet and Iceberg tables are opened in Python and handed to DuckDB as Arrow.
76
+ - `delta:` and `parquet:` paths must sit inside `landing.locations`. `..` segments,
77
+ symlinks that leave the folder and other buckets are refused.
78
+ - Iceberg tables are reached by name through the catalog, never by path.
79
+ - A DuckLake is attached read-only. DuckDB reads its Parquet files itself, so the
80
+ lake's data folder, and only that folder, stays readable from SQL.
81
+
82
+ ## Limits
83
+
84
+ - Iceberg tables are loaded into memory before the diff. Keep them to what fits, or
85
+ compare a snapshot or partition you copy elsewhere, until pushdown lands.
86
+ - DuckLake needs DuckDB's `ducklake` extension, and `httpfs` when its files live in S3.
87
+ DuckDB downloads them on first use; install them once if you work offline.
88
+ - The warehouse can be a DuckDB file or Redshift; see [redshift.md](redshift.md).
89
+
90
+ ## Testing without AWS
91
+
92
+ `pytest` runs every S3 and Glue test against [moto](https://github.com/getmoto/moto)'s
93
+ server, in process. To run them against a real S3-compatible server too:
94
+
95
+ ```bash
96
+ docker run -d -p 9000:9000 -e MINIO_ROOT_USER=lazarillo -e MINIO_ROOT_PASSWORD=lazarillo-secret bitnamilegacy/minio
97
+ LAZARILLO_TEST_S3_ENDPOINT=http://127.0.0.1:9000 LAZARILLO_TEST_S3_KEY=lazarillo \
98
+ LAZARILLO_TEST_S3_SECRET=lazarillo-secret pytest -m minio
99
+ ```
100
+
101
+ MinIO has no Glue, so there the Iceberg tests use a SQL catalog whose data lives in the bucket.
@@ -0,0 +1,26 @@
1
+ # checkride (design notes)
2
+
3
+ A *checkride* is the practical exam a pilot flies with an examiner before getting a rating.
4
+ `lazarillo checkride` will put agents through the same thing on data work.
5
+
6
+ ## Idea
7
+
8
+ Every task starts from a broken but realistic platform (the Tarima Tickets demo in
9
+ `examples/tarima-tickets` plus a scenario patch). The agent has to reach a verifiable end state. Each task is scored by
10
+ checks the harness can run, never by an LLM judge:
11
+
12
+ | task | the agent must… | pass when |
13
+ |---|---|---|
14
+ | `incremental-drift` | find why payouts are overstated and fix `fct_orders` | `verify fct_orders` reports identical |
15
+ | `late-arrivals` | explain the source/landing gap | answer names the watermark and the 35 missing box office orders |
16
+ | `iceberg-parity` | decide whether the Iceberg table can replace Delta | answer is "no" and names the timestamp shift |
17
+ | `safe-refactor` | rename a column in staging without breaking the dashboard | dbt build passes, exposure columns unchanged |
18
+
19
+ Each task runs twice, **with** the harness (MCP tools) and **without** it (raw SQL
20
+ access), for every model under test. The headline number is how much the harness helps.
21
+
22
+ ## Open questions
23
+
24
+ - Scenario format: a YAML file plus a patch applied to the demo project?
25
+ - Isolation: a fresh copy of `examples/tarima-tickets/data` per run.
26
+ - Reporting: Markdown table plus JSON, easy to publish.