dbdocs 1.0.2__tar.gz → 1.2.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.
- {dbdocs-1.0.2 → dbdocs-1.2.0}/.gitignore +2 -0
- dbdocs-1.2.0/PKG-INFO +93 -0
- dbdocs-1.2.0/README.md +69 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/cli/main.py +17 -5
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/core/config.py +25 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/extract/column_lineage.py +153 -67
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/extract/erd.py +10 -1
- dbdocs-1.2.0/dbdocs/extract/health/__init__.py +12 -0
- dbdocs-1.2.0/dbdocs/extract/health/dimensions.py +262 -0
- dbdocs-1.2.0/dbdocs/extract/health/extractor.py +299 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/__init__.py +106 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/base.py +43 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/dimensions/__init__.py +7 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/dimensions/documentation.py +71 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/dimensions/governance.py +75 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/dimensions/modeling.py +241 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/dimensions/performance.py +65 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/dimensions/structure.py +79 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/dimensions/testing.py +51 -0
- dbdocs-1.2.0/dbdocs/extract/health/rules/registry.py +155 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/builder.py +84 -11
- dbdocs-1.2.0/dbdocs/site/bundle/assets/css/icons.css +35 -0
- {dbdocs-1.0.2/dbdocs/site/bundle/assets → dbdocs-1.2.0/dbdocs/site/bundle/assets/css}/style.css +218 -4
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/bundle/assets/graph/index.css +1 -1
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/bundle/assets/graph/index.js +12 -12
- dbdocs-1.2.0/dbdocs/site/bundle/assets/js/app.js +20 -0
- dbdocs-1.2.0/dbdocs/site/bundle/assets/js/data.js +63 -0
- dbdocs-1.2.0/dbdocs/site/bundle/assets/js/service.js +234 -0
- dbdocs-1.2.0/dbdocs/site/bundle/assets/js/ui.js +866 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/bundle/index.html +18 -4
- dbdocs-1.2.0/dbdocs/site/inject.py +16 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/pyproject.toml +3 -1
- dbdocs-1.0.2/PKG-INFO +0 -78
- dbdocs-1.0.2/README.md +0 -56
- dbdocs-1.0.2/dbdocs/site/bundle/assets/app.js +0 -504
- dbdocs-1.0.2/dbdocs/site/inject.py +0 -32
- {dbdocs-1.0.2 → dbdocs-1.2.0}/LICENSE +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/__init__.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/__main__.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/cli/__init__.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/core/__init__.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/core/artifacts.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/core/exceptions.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/core/log.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/extract/__init__.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/extract/_sqlglot_lineage.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/extract/erd_json.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/extract/graph.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/extract/nodes.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/main.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/__init__.py +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/bundle/assets/favicon.svg +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/bundle/assets/vendor/marked.min.js +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/bundle/assets/vendor/minisearch.min.js +0 -0
- {dbdocs-1.0.2 → dbdocs-1.2.0}/dbdocs/site/deploy.py +0 -0
dbdocs-1.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dbdocs
|
|
3
|
+
Version: 1.2.0
|
|
4
|
+
Summary: Alternative dbt docs site: Catalog + ERD + column-level lineage
|
|
5
|
+
Project-URL: Homepage, https://github.com/datnguye/dbt-docs
|
|
6
|
+
Project-URL: Repository, https://github.com/datnguye/dbt-docs
|
|
7
|
+
Author-email: Dat Nguyen <datnguyen.it09@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: dbt,dbterd,documentation,erd,lineage,sqlglot
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
14
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: artifact-parser[dbt]>=1.0.0
|
|
18
|
+
Requires-Dist: click>=8.1
|
|
19
|
+
Requires-Dist: dbterd>=1.27
|
|
20
|
+
Requires-Dist: pydantic>=2.13.4
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Requires-Dist: sqlglot>=25
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<img src="docs/assets/logo.svg" alt="dbdocs logo" width="220" height="88">
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<p align="center"><b>An alternative dbt docs site — catalog + ERD + column-level lineage + versioned deploys, all in one CLI.</b></p>
|
|
30
|
+
|
|
31
|
+
<p align="center">
|
|
32
|
+
<a href="https://dbdocs.datnguye.me/latest/demo/latest/"><img src="https://img.shields.io/badge/live-demo-FF694A?style=flat&logo=rocket&logoColor=white" alt="live demo"></a>
|
|
33
|
+
<a href="https://dbdocs.datnguye.me/"><img src="https://img.shields.io/badge/docs-visit%20site-blue?style=flat&logo=gitbook&logoColor=white" alt="docs"></a>
|
|
34
|
+
<a href="https://pypi.org/project/dbdocs/"><img src="https://badge.fury.io/py/dbdocs.svg" alt="PyPI version"></a>
|
|
35
|
+
<img src="https://img.shields.io/badge/CLI-Python-FFCE3E?labelColor=14354C&logo=python&logoColor=white" alt="python-cli">
|
|
36
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
37
|
+
<a href="https://www.python.org"><img src="https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13-3776AB.svg?style=flat&logo=python&logoColor=white" alt="python"></a>
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
Turn your dbt artifacts into a self-contained docs site: a browsable catalog, an entity-relationship diagram, an interactive lineage DAG, and **column-level lineage** traced from your compiled SQL — all in one `dbdocs generate`. Serve it with `dbdocs serve`, or deploy versioned builds anywhere a static host will take them.
|
|
41
|
+
|
|
42
|
+
| Catalog | Model page | Lineage DAG | Health Check |
|
|
43
|
+
|---|---|---|---|
|
|
44
|
+
|  |  |  |  |
|
|
45
|
+
|
|
46
|
+
## Why dbdocs?
|
|
47
|
+
|
|
48
|
+
dbt's built-in docs are great — right up until you want to know *which upstream column fed this downstream column*, or *which tables relate to each other*, or *what changed between last week's docs and today's*. dbdocs fills all three gaps without asking you to install a documentation framework or maintain a separate ERD tool.
|
|
49
|
+
|
|
50
|
+
**What you get that nothing else bundles together:**
|
|
51
|
+
|
|
52
|
+
- **ERD + column-level lineage, side by side.** The entity-relationship diagram (powered by [dbterd](https://github.com/datnguye/dbterd)) shows table relationships; column lineage (traced by [sqlglot](https://github.com/tobymao/sqlglot) from compiled SQL) shows exactly which column fed which. Most alternatives give you one or the other — dbdocs gives you both.
|
|
53
|
+
- **Column impact analysis.** Select any column and see its downstream dependents across the project, so you know what a schema change will break before you run it.
|
|
54
|
+
- **Deep-link URLs.** Every focused node, column, and filtered DAG view has a shareable URL. Paste it in Slack and your teammate lands on exactly the right model, column, or graph state.
|
|
55
|
+
- **Any sqlglot-supported dialect.** The dialect for column-lineage parsing is auto-detected from your manifest's `adapter_type` (Snowflake, BigQuery, Redshift, DuckDB, PostgreSQL, Databricks/Spark, Trino, and more — anything sqlglot understands). Override it per-project with `dialect:` in `dbdocs.yml` when auto-detection isn't enough.
|
|
56
|
+
- **Scales without freezing.** Column-lineage parsing fans out across CPU cores automatically above 500 models, so large projects finish in roughly the same wall-clock time as small ones. The DAG is windowed by React Flow, so a 1 000-model graph doesn't turn your browser into a space heater. The payload ships as an external gzip (`dbdocs-data.json.gz`, decompressed client-side) so `index.html` stays tiny regardless of project size.
|
|
57
|
+
- **Fail-soft.** One model with SQL sqlglot can't parse gets skipped and logged — it never sinks the whole generate run.
|
|
58
|
+
- **Project Health Check.** A scorecard across the six [dbt-project-evaluator](https://dbt-labs.github.io/dbt-project-evaluator/) dimensions (testing, modeling, documentation, structure, performance, governance), computed straight from your `manifest.json` — no extra dbt package, no warehouse. When a `run_results.json` is also present (any `dbt build`/`dbt test`; default `<target_dir>/run_results.json`, override with `--run-results`), each test additionally shows up as a pass/fail finding grouped by what it checks (integrity, referential, validity, business logic, freshness). dbdocs only reads the artifacts — it never runs dbt or touches your warehouse. Fail-soft: a missing `run_results.json` just drops the per-test detail; the dimensions still render.
|
|
59
|
+
- **Versioned deploys, no plugins.** `dbdocs deploy --version v1.2 --alias latest` generates into a plain directory tree, writes a `versions.json` index, and the SPA renders a version dropdown. No mike, no external tooling, no surprise dependencies.
|
|
60
|
+
- **Catalog navigation + client-side search.** Models, seeds, and snapshots grouped by database and schema; full-text search without a backend.
|
|
61
|
+
- **Dark / light theme.**
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install dbdocs --upgrade
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Requires Python 3.10+.
|
|
70
|
+
|
|
71
|
+
## Quickstart
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
dbt docs generate # writes target/manifest.json + target/catalog.json
|
|
75
|
+
dbdocs generate # builds ./site/ with index.html + dbdocs-data.json.gz
|
|
76
|
+
dbdocs serve # static http server on http://127.0.0.1:8000
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The site must be served over HTTP (not opened as a local file) because it fetches the data payload at load time. `dbdocs serve` handles that locally; any static host works for deployment.
|
|
80
|
+
|
|
81
|
+
Full walkthrough, configuration, and architecture live in the **[documentation](https://dbdocs.datnguye.me/)**.
|
|
82
|
+
|
|
83
|
+
## Contributing
|
|
84
|
+
|
|
85
|
+
Contributions are welcome — bugs, features, docs, typos. See the **[Contributing Guide](https://dbdocs.datnguye.me/latest/nav/development/contributing-guide.html)**.
|
|
86
|
+
|
|
87
|
+
If dbdocs saves you some clicks, consider [buying me a coffee](https://www.buymeacoffee.com/datnguye).
|
|
88
|
+
|
|
89
|
+
<a href="https://www.buymeacoffee.com/datnguye"><img src="https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?logo=buy-me-a-coffee&logoColor=white&labelColor=ff813f&style=for-the-badge" alt="buy me a coffee"></a>
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
[MIT](./LICENSE) © Dat Nguyen
|
dbdocs-1.2.0/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/assets/logo.svg" alt="dbdocs logo" width="220" height="88">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center"><b>An alternative dbt docs site — catalog + ERD + column-level lineage + versioned deploys, all in one CLI.</b></p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://dbdocs.datnguye.me/latest/demo/latest/"><img src="https://img.shields.io/badge/live-demo-FF694A?style=flat&logo=rocket&logoColor=white" alt="live demo"></a>
|
|
9
|
+
<a href="https://dbdocs.datnguye.me/"><img src="https://img.shields.io/badge/docs-visit%20site-blue?style=flat&logo=gitbook&logoColor=white" alt="docs"></a>
|
|
10
|
+
<a href="https://pypi.org/project/dbdocs/"><img src="https://badge.fury.io/py/dbdocs.svg" alt="PyPI version"></a>
|
|
11
|
+
<img src="https://img.shields.io/badge/CLI-Python-FFCE3E?labelColor=14354C&logo=python&logoColor=white" alt="python-cli">
|
|
12
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
13
|
+
<a href="https://www.python.org"><img src="https://img.shields.io/badge/Python-3.10|3.11|3.12|3.13-3776AB.svg?style=flat&logo=python&logoColor=white" alt="python"></a>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
Turn your dbt artifacts into a self-contained docs site: a browsable catalog, an entity-relationship diagram, an interactive lineage DAG, and **column-level lineage** traced from your compiled SQL — all in one `dbdocs generate`. Serve it with `dbdocs serve`, or deploy versioned builds anywhere a static host will take them.
|
|
17
|
+
|
|
18
|
+
| Catalog | Model page | Lineage DAG | Health Check |
|
|
19
|
+
|---|---|---|---|
|
|
20
|
+
|  |  |  |  |
|
|
21
|
+
|
|
22
|
+
## Why dbdocs?
|
|
23
|
+
|
|
24
|
+
dbt's built-in docs are great — right up until you want to know *which upstream column fed this downstream column*, or *which tables relate to each other*, or *what changed between last week's docs and today's*. dbdocs fills all three gaps without asking you to install a documentation framework or maintain a separate ERD tool.
|
|
25
|
+
|
|
26
|
+
**What you get that nothing else bundles together:**
|
|
27
|
+
|
|
28
|
+
- **ERD + column-level lineage, side by side.** The entity-relationship diagram (powered by [dbterd](https://github.com/datnguye/dbterd)) shows table relationships; column lineage (traced by [sqlglot](https://github.com/tobymao/sqlglot) from compiled SQL) shows exactly which column fed which. Most alternatives give you one or the other — dbdocs gives you both.
|
|
29
|
+
- **Column impact analysis.** Select any column and see its downstream dependents across the project, so you know what a schema change will break before you run it.
|
|
30
|
+
- **Deep-link URLs.** Every focused node, column, and filtered DAG view has a shareable URL. Paste it in Slack and your teammate lands on exactly the right model, column, or graph state.
|
|
31
|
+
- **Any sqlglot-supported dialect.** The dialect for column-lineage parsing is auto-detected from your manifest's `adapter_type` (Snowflake, BigQuery, Redshift, DuckDB, PostgreSQL, Databricks/Spark, Trino, and more — anything sqlglot understands). Override it per-project with `dialect:` in `dbdocs.yml` when auto-detection isn't enough.
|
|
32
|
+
- **Scales without freezing.** Column-lineage parsing fans out across CPU cores automatically above 500 models, so large projects finish in roughly the same wall-clock time as small ones. The DAG is windowed by React Flow, so a 1 000-model graph doesn't turn your browser into a space heater. The payload ships as an external gzip (`dbdocs-data.json.gz`, decompressed client-side) so `index.html` stays tiny regardless of project size.
|
|
33
|
+
- **Fail-soft.** One model with SQL sqlglot can't parse gets skipped and logged — it never sinks the whole generate run.
|
|
34
|
+
- **Project Health Check.** A scorecard across the six [dbt-project-evaluator](https://dbt-labs.github.io/dbt-project-evaluator/) dimensions (testing, modeling, documentation, structure, performance, governance), computed straight from your `manifest.json` — no extra dbt package, no warehouse. When a `run_results.json` is also present (any `dbt build`/`dbt test`; default `<target_dir>/run_results.json`, override with `--run-results`), each test additionally shows up as a pass/fail finding grouped by what it checks (integrity, referential, validity, business logic, freshness). dbdocs only reads the artifacts — it never runs dbt or touches your warehouse. Fail-soft: a missing `run_results.json` just drops the per-test detail; the dimensions still render.
|
|
35
|
+
- **Versioned deploys, no plugins.** `dbdocs deploy --version v1.2 --alias latest` generates into a plain directory tree, writes a `versions.json` index, and the SPA renders a version dropdown. No mike, no external tooling, no surprise dependencies.
|
|
36
|
+
- **Catalog navigation + client-side search.** Models, seeds, and snapshots grouped by database and schema; full-text search without a backend.
|
|
37
|
+
- **Dark / light theme.**
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install dbdocs --upgrade
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10+.
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
dbt docs generate # writes target/manifest.json + target/catalog.json
|
|
51
|
+
dbdocs generate # builds ./site/ with index.html + dbdocs-data.json.gz
|
|
52
|
+
dbdocs serve # static http server on http://127.0.0.1:8000
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The site must be served over HTTP (not opened as a local file) because it fetches the data payload at load time. `dbdocs serve` handles that locally; any static host works for deployment.
|
|
56
|
+
|
|
57
|
+
Full walkthrough, configuration, and architecture live in the **[documentation](https://dbdocs.datnguye.me/)**.
|
|
58
|
+
|
|
59
|
+
## Contributing
|
|
60
|
+
|
|
61
|
+
Contributions are welcome — bugs, features, docs, typos. See the **[Contributing Guide](https://dbdocs.datnguye.me/latest/nav/development/contributing-guide.html)**.
|
|
62
|
+
|
|
63
|
+
If dbdocs saves you some clicks, consider [buying me a coffee](https://www.buymeacoffee.com/datnguye).
|
|
64
|
+
|
|
65
|
+
<a href="https://www.buymeacoffee.com/datnguye"><img src="https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg?logo=buy-me-a-coffee&logoColor=white&labelColor=ff813f&style=for-the-badge" alt="buy me a coffee"></a>
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
[MIT](./LICENSE) © Dat Nguyen
|
|
@@ -26,7 +26,10 @@ __version__ = importlib.metadata.version("dbdocs")
|
|
|
26
26
|
@click.pass_context
|
|
27
27
|
def dbdocs(ctx, config_path):
|
|
28
28
|
"""Alternative dbt docs site: dbt docs + ERD + column-level lineage."""
|
|
29
|
-
|
|
29
|
+
# The version banner is useful for the build commands (generate/deploy) but
|
|
30
|
+
# is noise for the long-running `serve`, which prints its own start line.
|
|
31
|
+
if ctx.invoked_subcommand != "serve":
|
|
32
|
+
logger.info("Run with dbdocs==%s", __version__)
|
|
30
33
|
try:
|
|
31
34
|
ctx.obj = DbDocsConfig.load(config_path)
|
|
32
35
|
except DbDocsError as exc:
|
|
@@ -38,16 +41,25 @@ def dbdocs(ctx, config_path):
|
|
|
38
41
|
@click.option(
|
|
39
42
|
"--dialect", default=None, help="SQL dialect for column lineage (default: adapter_type)."
|
|
40
43
|
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--run-results",
|
|
46
|
+
default=None,
|
|
47
|
+
help=(
|
|
48
|
+
"Path to run_results.json for the Health Check (default: <target_dir>/run_results.json)."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
41
51
|
@click.pass_obj
|
|
42
|
-
def generate(config: DbDocsConfig, output_dir, dialect):
|
|
43
|
-
"""Build the
|
|
52
|
+
def generate(config: DbDocsConfig, output_dir, dialect, run_results):
|
|
53
|
+
"""Build the site from dbt artifacts (served over HTTP; data loaded externally)."""
|
|
44
54
|
if dialect is not None:
|
|
45
55
|
config.dialect = dialect
|
|
56
|
+
if run_results is not None:
|
|
57
|
+
config.run_results = run_results
|
|
46
58
|
try:
|
|
47
|
-
|
|
59
|
+
ReportBuilder(config).generate(output_dir=output_dir)
|
|
48
60
|
except DbDocsError as exc:
|
|
49
61
|
raise click.ClickException(str(exc)) from exc
|
|
50
|
-
|
|
62
|
+
# The builder already logs "Generated site at <path> (N nodes, M edges)".
|
|
51
63
|
|
|
52
64
|
|
|
53
65
|
@dbdocs.command(name="serve")
|
|
@@ -49,6 +49,13 @@ class DbDocsConfig:
|
|
|
49
49
|
#: Project README rendered on the overview (relative to the working dir). Set
|
|
50
50
|
#: empty to omit the README section. Missing file ⇒ section simply absent.
|
|
51
51
|
readme: str = "README.md"
|
|
52
|
+
#: Custom header logo (path relative to the working dir). Empty ⇒ the bundled
|
|
53
|
+
#: default mark. A missing file or a path escaping the cwd is ignored
|
|
54
|
+
#: (fail-soft, like ``readme``) ⇒ the default mark.
|
|
55
|
+
logo: str = ""
|
|
56
|
+
#: Custom favicon (path relative to the working dir). Empty ⇒ the bundled
|
|
57
|
+
#: default favicon. Same fail-soft handling as ``logo``.
|
|
58
|
+
favicon: str = ""
|
|
52
59
|
target_dir: str = "target"
|
|
53
60
|
#: Where the generated site is written. Nested under the dbt ``target/`` by
|
|
54
61
|
#: default so docs sit alongside the artifacts they're built from.
|
|
@@ -62,6 +69,18 @@ class DbDocsConfig:
|
|
|
62
69
|
#: ``resource_type``, …) passed straight to ``DbtErd``. Configured here so the
|
|
63
70
|
#: ERD shape lives in ``dbdocs.yml`` rather than a separate ``.dbterd.yml``.
|
|
64
71
|
dbterd: dict = field(default_factory=dict)
|
|
72
|
+
#: Path to the ``run_results.json`` parsed for the Health Check (always on).
|
|
73
|
+
#: If ``None`` (the default), ``<target_dir>/run_results.json`` is used. A
|
|
74
|
+
#: relative path is resolved against the cwd; a path that escapes the cwd is
|
|
75
|
+
#: silently ignored (fail-soft, like ``readme`` / ``logo``). When the file is
|
|
76
|
+
#: absent the Health Check page is simply empty.
|
|
77
|
+
run_results: "str | None" = None
|
|
78
|
+
#: Health Check rule engine config (all optional):
|
|
79
|
+
#: ``thresholds`` (e.g. ``model_fanout: 5``), ``disable`` (rule names to skip),
|
|
80
|
+
#: ``disable_dimensions`` (whole dimensions to skip), and ``rules_module`` (a
|
|
81
|
+
#: dotted import path whose ``register_rule`` calls add custom rules). Entry
|
|
82
|
+
#: points under ``dbdocs.health_rules`` are also discovered automatically.
|
|
83
|
+
health: dict = field(default_factory=dict)
|
|
65
84
|
|
|
66
85
|
@classmethod
|
|
67
86
|
def load(cls, path: "str | Path | None" = None) -> "DbDocsConfig":
|
|
@@ -92,6 +111,8 @@ class DbDocsConfig:
|
|
|
92
111
|
return cls(**raw)
|
|
93
112
|
|
|
94
113
|
#: Build-control fields that are not part of the site's display metadata.
|
|
114
|
+
#: ``logo``/``favicon`` are excluded too — they're *source* paths; the builder
|
|
115
|
+
#: copies the files into the output and injects their *deployed* asset URLs.
|
|
95
116
|
_NON_METADATA_FIELDS = (
|
|
96
117
|
"target_dir",
|
|
97
118
|
"output_dir",
|
|
@@ -99,6 +120,10 @@ class DbDocsConfig:
|
|
|
99
120
|
"default_version",
|
|
100
121
|
"dbterd",
|
|
101
122
|
"readme",
|
|
123
|
+
"logo",
|
|
124
|
+
"favicon",
|
|
125
|
+
"run_results",
|
|
126
|
+
"health",
|
|
102
127
|
)
|
|
103
128
|
|
|
104
129
|
def render_context(self) -> dict:
|
|
@@ -12,6 +12,7 @@ must never sink the whole ``generate`` — it's caught, logged, and skipped, and
|
|
|
12
12
|
the run reports how many were skipped.
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
15
16
|
from typing import Any
|
|
16
17
|
|
|
17
18
|
from sqlglot import exp
|
|
@@ -27,6 +28,14 @@ _DIALECT_ALIASES = {
|
|
|
27
28
|
"databricks": "spark",
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
#: Parse + qualify is CPU-bound and embarrassingly parallel across models. Below
|
|
32
|
+
#: this model count the ProcessPoolExecutor's spawn + pickle overhead outweighs
|
|
33
|
+
#: the win, so we stay serial; at or above it we fan out across cores.
|
|
34
|
+
_PARALLEL_THRESHOLD = 500
|
|
35
|
+
|
|
36
|
+
#: Errors a single model's lineage may raise that must not sink the whole run.
|
|
37
|
+
_MODEL_ERRORS = (SqlglotError, LineageError, KeyError, ValueError, RecursionError)
|
|
38
|
+
|
|
30
39
|
|
|
31
40
|
def _to_dialect(adapter_type: "str | None") -> "str | None":
|
|
32
41
|
if not adapter_type:
|
|
@@ -34,6 +43,95 @@ def _to_dialect(adapter_type: "str | None") -> "str | None":
|
|
|
34
43
|
return _DIALECT_ALIASES.get(adapter_type, adapter_type)
|
|
35
44
|
|
|
36
45
|
|
|
46
|
+
def _map_table(table: exp.Table, relation_to_node: dict) -> "str | None":
|
|
47
|
+
"""Map a sqlglot ``Table`` to a dbt unique_id via the relation index."""
|
|
48
|
+
candidates = [
|
|
49
|
+
f"{table.catalog}.{table.db}.{table.name}",
|
|
50
|
+
f"{table.db}.{table.name}",
|
|
51
|
+
table.name,
|
|
52
|
+
]
|
|
53
|
+
for candidate in candidates:
|
|
54
|
+
mapped = relation_to_node.get(candidate.lower().strip("."))
|
|
55
|
+
if mapped:
|
|
56
|
+
return mapped
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _leaf_columns(root: Node, relation_to_node: dict) -> list:
|
|
61
|
+
"""Collect distinct upstream ``{node, column}`` leaves of a lineage tree.
|
|
62
|
+
|
|
63
|
+
A leaf is a node whose source is a real ``Table`` (not a CTE/subquery scope)
|
|
64
|
+
that maps back to a dbt node. The root itself is skipped.
|
|
65
|
+
"""
|
|
66
|
+
seen = set()
|
|
67
|
+
upstream = []
|
|
68
|
+
for node in root.walk():
|
|
69
|
+
if node is root:
|
|
70
|
+
continue
|
|
71
|
+
source = node.source
|
|
72
|
+
if not isinstance(source, exp.Table):
|
|
73
|
+
continue
|
|
74
|
+
mapped = _map_table(source, relation_to_node)
|
|
75
|
+
if mapped is None:
|
|
76
|
+
continue
|
|
77
|
+
column = node_name(node.name)
|
|
78
|
+
key = (mapped, column)
|
|
79
|
+
if key in seen:
|
|
80
|
+
continue
|
|
81
|
+
seen.add(key)
|
|
82
|
+
upstream.append({"node": mapped, "column": column})
|
|
83
|
+
return upstream
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _extract_model_columns(
|
|
87
|
+
unique_id: str,
|
|
88
|
+
compiled: str,
|
|
89
|
+
output_columns: list,
|
|
90
|
+
schema: dict,
|
|
91
|
+
dialect: "str | None",
|
|
92
|
+
relation_to_node: dict,
|
|
93
|
+
) -> dict:
|
|
94
|
+
"""Trace every output column of one model to its upstream columns.
|
|
95
|
+
|
|
96
|
+
Pure and picklable (plain str/list/dict args, no manifest/catalog objects) so
|
|
97
|
+
it runs unchanged in the serial loop or a ``ProcessPoolExecutor`` worker.
|
|
98
|
+
Builds the scope once per model — qualify is the expensive part and is
|
|
99
|
+
identical for every column — then traces each column against it.
|
|
100
|
+
"""
|
|
101
|
+
out: dict = {}
|
|
102
|
+
scope = prepare_scope(compiled, schema=schema, dialect=dialect)
|
|
103
|
+
for column in output_columns:
|
|
104
|
+
try:
|
|
105
|
+
root = lineage(column, compiled, dialect=dialect, scope=scope)
|
|
106
|
+
except SqlglotError:
|
|
107
|
+
# One unresolvable column shouldn't drop the rest of the model.
|
|
108
|
+
continue
|
|
109
|
+
upstream = _leaf_columns(root, relation_to_node)
|
|
110
|
+
if upstream:
|
|
111
|
+
out[f"{unique_id}.{column}"] = upstream
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _extract_model_task(work: tuple) -> tuple:
|
|
116
|
+
"""Worker wrapper: returns ``(unique_id, columns_map, error_str_or_None)``.
|
|
117
|
+
|
|
118
|
+
Catching here (rather than letting the exception cross the process boundary)
|
|
119
|
+
keeps the fail-soft contract: one unparseable model is reported and skipped,
|
|
120
|
+
never sinking the pool.
|
|
121
|
+
"""
|
|
122
|
+
unique_id, compiled, output_columns, schema, dialect, relation_to_node = work
|
|
123
|
+
try:
|
|
124
|
+
return (
|
|
125
|
+
unique_id,
|
|
126
|
+
_extract_model_columns(
|
|
127
|
+
unique_id, compiled, output_columns, schema, dialect, relation_to_node
|
|
128
|
+
),
|
|
129
|
+
None,
|
|
130
|
+
)
|
|
131
|
+
except _MODEL_ERRORS as exc:
|
|
132
|
+
return unique_id, {}, str(exc)
|
|
133
|
+
|
|
134
|
+
|
|
37
135
|
class ColumnLineageExtractor:
|
|
38
136
|
"""Build the ``columnLineage`` map for a dbt project's models.
|
|
39
137
|
|
|
@@ -54,86 +152,74 @@ class ColumnLineageExtractor:
|
|
|
54
152
|
self.skipped = 0
|
|
55
153
|
|
|
56
154
|
def extract(self) -> dict:
|
|
57
|
-
"""Return the ``columnLineage`` map across all models (fail-soft).
|
|
155
|
+
"""Return the ``columnLineage`` map across all models (fail-soft).
|
|
156
|
+
|
|
157
|
+
Runs serially for small projects; fans the per-model sqlglot work across
|
|
158
|
+
processes once a project has ``_PARALLEL_THRESHOLD`` models or more, where
|
|
159
|
+
the CPU-bound parse/qualify dominates the pool's spawn cost.
|
|
160
|
+
"""
|
|
161
|
+
work = self._work_items()
|
|
162
|
+
if not work:
|
|
163
|
+
return {}
|
|
164
|
+
if len(work) >= _PARALLEL_THRESHOLD:
|
|
165
|
+
results = self._extract_parallel(work)
|
|
166
|
+
else:
|
|
167
|
+
results = (_extract_model_task(item) for item in work)
|
|
168
|
+
|
|
58
169
|
result: dict = {}
|
|
170
|
+
for unique_id, columns, error in results:
|
|
171
|
+
if error is not None:
|
|
172
|
+
self.skipped += 1
|
|
173
|
+
logger.warning("Column lineage skipped for %s: %s", node_name(unique_id), error)
|
|
174
|
+
continue
|
|
175
|
+
result.update(columns)
|
|
176
|
+
if self.skipped:
|
|
177
|
+
logger.info("Column lineage: skipped %s model(s) that failed to parse.", self.skipped)
|
|
178
|
+
return result
|
|
179
|
+
|
|
180
|
+
def _extract_parallel(self, work: list) -> list:
|
|
181
|
+
"""Run the per-model tasks across a process pool, returning their results."""
|
|
182
|
+
with ProcessPoolExecutor() as pool:
|
|
183
|
+
return list(pool.map(_extract_model_task, work))
|
|
184
|
+
|
|
185
|
+
def _work_items(self) -> list:
|
|
186
|
+
"""Build the picklable per-model work tuples for the (serial or pool) run.
|
|
187
|
+
|
|
188
|
+
Each item is ``(unique_id, compiled, output_columns, schema, dialect,
|
|
189
|
+
relation_to_node)`` — all plain str/list/dict, so it crosses a process
|
|
190
|
+
boundary cleanly (the Pydantic manifest/catalog never do).
|
|
191
|
+
"""
|
|
192
|
+
work = []
|
|
59
193
|
for unique_id, model in (getattr(self.manifest, "nodes", {}) or {}).items():
|
|
60
194
|
if not str(unique_id).startswith("model."):
|
|
61
195
|
continue
|
|
62
196
|
compiled = getattr(model, "compiled_code", "") or ""
|
|
63
197
|
if not compiled.strip():
|
|
64
198
|
continue
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
199
|
+
output_columns = self._output_columns(unique_id, model)
|
|
200
|
+
if not output_columns:
|
|
201
|
+
continue
|
|
202
|
+
work.append(
|
|
203
|
+
(
|
|
204
|
+
unique_id,
|
|
205
|
+
compiled,
|
|
206
|
+
output_columns,
|
|
207
|
+
self.schema,
|
|
208
|
+
self.dialect,
|
|
209
|
+
self._relation_to_node,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
return work
|
|
73
213
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
output_columns = [c for c in (getattr(
|
|
77
|
-
# Fall back to the catalog's column list when the manifest has none.
|
|
214
|
+
def _output_columns(self, unique_id: str, model: Any) -> list:
|
|
215
|
+
"""The model's documented columns, falling back to the catalog's list."""
|
|
216
|
+
output_columns = [c for c in (getattr(model, "columns", {}) or {})]
|
|
78
217
|
if not output_columns:
|
|
79
218
|
catalog_node = (getattr(self.catalog, "nodes", {}) or {}).get(unique_id)
|
|
80
219
|
output_columns = (
|
|
81
220
|
list(getattr(catalog_node, "columns", {}) or {}) if catalog_node else []
|
|
82
221
|
)
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
# Build the scope once per model, then trace each column against it —
|
|
86
|
-
# qualify is the expensive part and is identical for every column.
|
|
87
|
-
scope = prepare_scope(compiled, schema=self.schema, dialect=self.dialect)
|
|
88
|
-
for column in output_columns:
|
|
89
|
-
try:
|
|
90
|
-
root = lineage(column, compiled, dialect=self.dialect, scope=scope)
|
|
91
|
-
except SqlglotError:
|
|
92
|
-
# One unresolvable column shouldn't drop the rest of the model.
|
|
93
|
-
continue
|
|
94
|
-
upstream = self._leaf_columns(root)
|
|
95
|
-
if upstream:
|
|
96
|
-
result[f"{unique_id}.{column}"] = upstream
|
|
97
|
-
|
|
98
|
-
def _leaf_columns(self, root: Node) -> list:
|
|
99
|
-
"""Collect distinct upstream ``{node, column}`` leaves of a lineage tree.
|
|
100
|
-
|
|
101
|
-
A leaf is a node whose source is a real ``Table`` (not a CTE/subquery
|
|
102
|
-
scope) that we can map back to a dbt node. The root itself is skipped.
|
|
103
|
-
"""
|
|
104
|
-
seen = set()
|
|
105
|
-
upstream = []
|
|
106
|
-
for node in root.walk():
|
|
107
|
-
if node is root:
|
|
108
|
-
continue
|
|
109
|
-
source = node.source
|
|
110
|
-
if not isinstance(source, exp.Table):
|
|
111
|
-
continue
|
|
112
|
-
mapped = self._map_table(source)
|
|
113
|
-
if mapped is None:
|
|
114
|
-
continue
|
|
115
|
-
column = node_name(node.name)
|
|
116
|
-
key = (mapped, column)
|
|
117
|
-
if key in seen:
|
|
118
|
-
continue
|
|
119
|
-
seen.add(key)
|
|
120
|
-
upstream.append({"node": mapped, "column": column})
|
|
121
|
-
return upstream
|
|
122
|
-
|
|
123
|
-
def _map_table(self, table: exp.Table) -> "str | None":
|
|
124
|
-
catalog = table.catalog
|
|
125
|
-
db = table.db
|
|
126
|
-
name = table.name
|
|
127
|
-
candidates = [
|
|
128
|
-
f"{catalog}.{db}.{name}",
|
|
129
|
-
f"{db}.{name}",
|
|
130
|
-
name,
|
|
131
|
-
]
|
|
132
|
-
for candidate in candidates:
|
|
133
|
-
mapped = self._relation_to_node.get(candidate.lower().strip("."))
|
|
134
|
-
if mapped:
|
|
135
|
-
return mapped
|
|
136
|
-
return None
|
|
222
|
+
return output_columns
|
|
137
223
|
|
|
138
224
|
def _relation_index(self) -> dict:
|
|
139
225
|
"""Map ``db.schema.table`` (and shorter forms) → dbt unique_id."""
|
|
@@ -9,12 +9,21 @@ foreign-key edges between them, all keyed by dbt unique_id.
|
|
|
9
9
|
|
|
10
10
|
import json
|
|
11
11
|
|
|
12
|
-
from dbterd.api import DbtErd
|
|
12
|
+
from dbterd.api import DbtErd, default
|
|
13
13
|
|
|
14
14
|
# Importing the module registers the "json" target with dbterd's PluginRegistry.
|
|
15
15
|
from dbdocs.extract import erd_json # noqa: F401
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
def erd_algo(dbterd_options: "dict | None" = None) -> str:
|
|
19
|
+
"""The dbterd algorithm that detected the ERD relationships.
|
|
20
|
+
|
|
21
|
+
Reads the configured ``algo`` from the ``dbterd:`` block, falling back to
|
|
22
|
+
dbterd's own default. Surfaced into the SPA so it can explain an empty ERD.
|
|
23
|
+
"""
|
|
24
|
+
return (dbterd_options or {}).get("algo") or default.default_algo()
|
|
25
|
+
|
|
26
|
+
|
|
18
27
|
def build_erd(dbterd_options: "dict | None" = None, artifacts_dir: "str | None" = None) -> DbtErd:
|
|
19
28
|
"""Build the ERD generator (json target) from dbdocs' ``dbterd`` options.
|
|
20
29
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Project Health Check — the six dbt-project-evaluator dimensions.
|
|
2
|
+
|
|
3
|
+
The public entry point is :class:`HealthCheckExtractor`, which composes the
|
|
4
|
+
manifest-derived rule engine (:mod:`dbdocs.extract.health.dimensions` +
|
|
5
|
+
:mod:`dbdocs.extract.health.rules`) with the optional ``run_results.json``
|
|
6
|
+
per-test detail. Custom rules can be registered via
|
|
7
|
+
:func:`dbdocs.extract.health.rules.register_rule` (see the plugin docs).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dbdocs.extract.health.extractor import HealthCheckExtractor
|
|
11
|
+
|
|
12
|
+
__all__ = ["HealthCheckExtractor"]
|