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.
- lazarillo-0.1.0/.github/workflows/ci.yml +41 -0
- lazarillo-0.1.0/.github/workflows/release.yml +63 -0
- lazarillo-0.1.0/.gitignore +15 -0
- lazarillo-0.1.0/CHANGELOG.md +16 -0
- lazarillo-0.1.0/LICENSE +21 -0
- lazarillo-0.1.0/PKG-INFO +219 -0
- lazarillo-0.1.0/README.md +157 -0
- lazarillo-0.1.0/docs/aws.md +101 -0
- lazarillo-0.1.0/docs/checkride.md +26 -0
- lazarillo-0.1.0/docs/dbt-cloud.md +39 -0
- lazarillo-0.1.0/docs/redshift.md +108 -0
- lazarillo-0.1.0/docs/releasing.md +26 -0
- lazarillo-0.1.0/examples/tarima-tickets/README.md +16 -0
- lazarillo-0.1.0/examples/tarima-tickets/build_demo.py +189 -0
- lazarillo-0.1.0/examples/tarima-tickets/lazarillo.yml +23 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/dbt_project.yml +13 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/macros/generate_schema_name.sql +4 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/models/marts/_marts.yml +37 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/models/marts/fct_event_revenue.sql +9 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/models/marts/fct_orders.sql +25 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/models/staging/_sources.yml +9 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/models/staging/stg_events.sql +8 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/models/staging/stg_orders.sql +11 -0
- lazarillo-0.1.0/examples/tarima-tickets/tarima/profiles.yml +14 -0
- lazarillo-0.1.0/pyproject.toml +66 -0
- lazarillo-0.1.0/src/lazarillo/__init__.py +3 -0
- lazarillo-0.1.0/src/lazarillo/cli.py +173 -0
- lazarillo-0.1.0/src/lazarillo/config.py +206 -0
- lazarillo-0.1.0/src/lazarillo/context.py +171 -0
- lazarillo-0.1.0/src/lazarillo/dbt_cloud.py +91 -0
- lazarillo-0.1.0/src/lazarillo/diff.py +140 -0
- lazarillo-0.1.0/src/lazarillo/engine.py +38 -0
- lazarillo-0.1.0/src/lazarillo/guardrails.py +55 -0
- lazarillo-0.1.0/src/lazarillo/init.py +110 -0
- lazarillo-0.1.0/src/lazarillo/lake.py +207 -0
- lazarillo-0.1.0/src/lazarillo/mcp_server.py +85 -0
- lazarillo-0.1.0/src/lazarillo/redshift.py +168 -0
- lazarillo-0.1.0/src/lazarillo/verify.py +94 -0
- lazarillo-0.1.0/src/lazarillo/warehouse.py +192 -0
- lazarillo-0.1.0/tests/conftest.py +17 -0
- lazarillo-0.1.0/tests/test_context.py +48 -0
- lazarillo-0.1.0/tests/test_dbt_cloud.py +179 -0
- lazarillo-0.1.0/tests/test_diff.py +25 -0
- lazarillo-0.1.0/tests/test_ducklake.py +59 -0
- lazarillo-0.1.0/tests/test_guardrails.py +53 -0
- lazarillo-0.1.0/tests/test_init.py +32 -0
- lazarillo-0.1.0/tests/test_lake.py +173 -0
- lazarillo-0.1.0/tests/test_redshift.py +298 -0
- 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.
|
lazarillo-0.1.0/LICENSE
ADDED
|
@@ -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.
|
lazarillo-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|