dbdocs 0.0.1__tar.gz → 1.0.1__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 (39) hide show
  1. dbdocs-1.0.1/.gitignore +168 -0
  2. {dbdocs-0.0.1 → dbdocs-1.0.1}/LICENSE +21 -21
  3. dbdocs-1.0.1/PKG-INFO +78 -0
  4. dbdocs-1.0.1/README.md +56 -0
  5. {dbdocs-0.0.1 → dbdocs-1.0.1}/dbdocs/__main__.py +3 -3
  6. dbdocs-1.0.1/dbdocs/cli/main.py +86 -0
  7. dbdocs-1.0.1/dbdocs/core/artifacts.py +82 -0
  8. dbdocs-1.0.1/dbdocs/core/config.py +136 -0
  9. dbdocs-1.0.1/dbdocs/core/exceptions.py +24 -0
  10. dbdocs-1.0.1/dbdocs/core/log.py +58 -0
  11. dbdocs-1.0.1/dbdocs/extract/__init__.py +0 -0
  12. dbdocs-1.0.1/dbdocs/extract/_sqlglot_lineage.py +287 -0
  13. dbdocs-1.0.1/dbdocs/extract/column_lineage.py +186 -0
  14. dbdocs-1.0.1/dbdocs/extract/erd.py +102 -0
  15. dbdocs-1.0.1/dbdocs/extract/erd_json.py +80 -0
  16. dbdocs-1.0.1/dbdocs/extract/graph.py +72 -0
  17. dbdocs-1.0.1/dbdocs/extract/nodes.py +119 -0
  18. {dbdocs-0.0.1 → dbdocs-1.0.1}/dbdocs/main.py +6 -6
  19. dbdocs-1.0.1/dbdocs/site/__init__.py +0 -0
  20. dbdocs-1.0.1/dbdocs/site/builder.py +141 -0
  21. dbdocs-1.0.1/dbdocs/site/bundle/assets/app.js +500 -0
  22. dbdocs-1.0.1/dbdocs/site/bundle/assets/favicon.svg +12 -0
  23. dbdocs-1.0.1/dbdocs/site/bundle/assets/graph/index.css +1 -0
  24. dbdocs-1.0.1/dbdocs/site/bundle/assets/graph/index.js +62 -0
  25. dbdocs-1.0.1/dbdocs/site/bundle/assets/style.css +289 -0
  26. dbdocs-1.0.1/dbdocs/site/bundle/assets/vendor/marked.min.js +6 -0
  27. dbdocs-1.0.1/dbdocs/site/bundle/assets/vendor/minisearch.min.js +8 -0
  28. dbdocs-1.0.1/dbdocs/site/bundle/index.html +48 -0
  29. dbdocs-1.0.1/dbdocs/site/deploy.py +140 -0
  30. dbdocs-1.0.1/dbdocs/site/inject.py +32 -0
  31. dbdocs-1.0.1/pyproject.toml +92 -0
  32. dbdocs-0.0.1/PKG-INFO +0 -46
  33. dbdocs-0.0.1/README.md +0 -19
  34. dbdocs-0.0.1/dbdocs/cli/main.py +0 -29
  35. dbdocs-0.0.1/dbdocs/helpers/log.py +0 -37
  36. dbdocs-0.0.1/pyproject.toml +0 -97
  37. {dbdocs-0.0.1 → dbdocs-1.0.1}/dbdocs/__init__.py +0 -0
  38. {dbdocs-0.0.1 → dbdocs-1.0.1}/dbdocs/cli/__init__.py +0 -0
  39. {dbdocs-0.0.1/dbdocs/helpers → dbdocs-1.0.1/dbdocs/core}/__init__.py +0 -0
@@ -0,0 +1,168 @@
1
+ # custom
2
+ /target
3
+ /samples/local
4
+ CHANGELOG.md
5
+ /dbt_packages
6
+
7
+ # Byte-compiled / optimized / DLL files
8
+ __pycache__/
9
+ *.py[cod]
10
+ *$py.class
11
+
12
+ # C extensions
13
+ *.so
14
+
15
+ # Distribution / packaging
16
+ .Python
17
+ build/
18
+ develop-eggs/
19
+ dist/
20
+ downloads/
21
+ eggs/
22
+ .eggs/
23
+ lib/
24
+ lib64/
25
+ parts/
26
+ sdist/
27
+ var/
28
+ wheels/
29
+ pip-wheel-metadata/
30
+ share/python-wheels/
31
+ *.egg-info/
32
+ .installed.cfg
33
+ *.egg
34
+ MANIFEST
35
+
36
+ # PyInstaller
37
+ # Usually these files are written by a python script from a template
38
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
39
+ *.manifest
40
+ *.spec
41
+
42
+ # Installer logs
43
+ pip-log.txt
44
+ pip-delete-this-directory.txt
45
+
46
+ # Unit test / coverage reports
47
+ htmlcov/
48
+ .tox/
49
+ .nox/
50
+ .coverage
51
+ .coverage.*
52
+ .cache
53
+ nosetests.xml
54
+ coverage.xml
55
+ *.cover
56
+ *.py,cover
57
+ .hypothesis/
58
+ .pytest_cache/
59
+
60
+ # Translations
61
+ *.mo
62
+ *.pot
63
+
64
+ # Django stuff:
65
+ *.log
66
+ local_settings.py
67
+ db.sqlite3
68
+ db.sqlite3-journal
69
+
70
+ # Flask stuff:
71
+ instance/
72
+ .webassets-cache
73
+
74
+ # Scrapy stuff:
75
+ .scrapy
76
+
77
+ # Sphinx documentation
78
+ docs/_build/
79
+
80
+ # PyBuilder
81
+ target/
82
+
83
+ # Jupyter Notebook
84
+ .ipynb_checkpoints
85
+
86
+ # IPython
87
+ profile_default/
88
+ ipython_config.py
89
+
90
+ # pyenv
91
+ .python-version
92
+
93
+ # pipenv
94
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
96
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
97
+ # install all needed dependencies.
98
+ #Pipfile.lock
99
+
100
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
101
+ __pypackages__/
102
+
103
+ # Celery stuff
104
+ celerybeat-schedule
105
+ celerybeat.pid
106
+
107
+ # SageMath parsed files
108
+ *.sage.py
109
+
110
+ # Environments
111
+ .env
112
+ .venv
113
+ env/
114
+ venv/
115
+ ENV/
116
+ env.bak/
117
+ venv.bak/
118
+
119
+ # Spyder project settings
120
+ .spyderproject
121
+ .spyproject
122
+
123
+ # Rope project settings
124
+ .ropeproject
125
+
126
+ # mkdocs documentation
127
+ /site
128
+ /site-docs
129
+ /demo-site
130
+ # Generated dbdocs demo, bundled into the docs site at /demo/ at build time.
131
+ /docs/demo
132
+
133
+ # mypy
134
+ .mypy_cache/
135
+ .dmypy.json
136
+ dmypy.json
137
+
138
+ # Pyre type checker
139
+ .pyre/
140
+
141
+ # Ruff
142
+ .ruff_cache/
143
+
144
+ # Local Claude settings + personal agent memory
145
+ .claude/settings.local.json
146
+ .claude/agent-memory-local/
147
+
148
+ # Frontend (React Flow graph bundle) — Node.js toolchain artifacts
149
+ node_modules/
150
+ frontend/node_modules/
151
+ frontend/dist/
152
+ frontend/.vite/
153
+ frontend/coverage/
154
+ frontend/*.tsbuildinfo
155
+ .eslintcache
156
+
157
+ # npm / yarn / pnpm logs & debug output
158
+ npm-debug.log*
159
+ yarn-debug.log*
160
+ yarn-error.log*
161
+ pnpm-debug.log*
162
+
163
+ # The BUILT graph bundle IS committed (shipped in the wheel) — do not ignore it.
164
+ !dbdocs/site/bundle/assets/graph/
165
+ !dbdocs/site/bundle/assets/graph/**
166
+
167
+ # dbdocs DEBUG file log
168
+ logs/
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 Dat Nguyen
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dat Nguyen
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.
dbdocs-1.0.1/PKG-INFO ADDED
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbdocs
3
+ Version: 1.0.1
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: click>=8.1
18
+ Requires-Dist: dbterd>=1.26
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: sqlglot>=25
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <img src="docs/assets/logo.svg" alt="dbdocs logo" width="220" height="88">
25
+ </p>
26
+
27
+ <p align="center"><b>An alternative dbt docs site — catalog + ERD + column-level lineage, baked into one file.</b></p>
28
+
29
+ <p align="center">
30
+ <a href="https://dbdocs.datnguye.me/latest/demo/"><img src="https://img.shields.io/badge/live-demo-FF694A?style=flat&logo=rocket&logoColor=white" alt="live demo"></a>
31
+ <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>
32
+ <a href="https://pypi.org/project/dbdocs/"><img src="https://badge.fury.io/py/dbdocs.svg" alt="PyPI version"></a>
33
+ <img src="https://img.shields.io/badge/CLI-Python-FFCE3E?labelColor=14354C&logo=python&logoColor=white" alt="python-cli">
34
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
35
+ <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>
36
+ </p>
37
+
38
+ Turn your dbt artifacts into a single self-contained `index.html`: a browsable catalog, an interactive lineage DAG and ERD, and **column-level lineage** from your compiled SQL. No server, no database, no build step — just a file you can open or host anywhere.
39
+
40
+ | Catalog | Model page | Lineage DAG |
41
+ |---|---|---|
42
+ | ![catalog](docs/assets/img/demo-catalog.png) | ![model page](docs/assets/img/demo-model-page.png) | ![dag](docs/assets/img/demo-model-dag.png) |
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ pip install dbdocs --upgrade
48
+ ```
49
+
50
+ Requires Python 3.10+.
51
+
52
+ ## Quickstart
53
+
54
+ ```bash
55
+ dbt docs generate # writes target/manifest.json + target/catalog.json
56
+ dbdocs generate # builds ./site/index.html with all data baked in
57
+ dbdocs serve # static http server on http://127.0.0.1:8000
58
+ ```
59
+
60
+ Full walkthrough, configuration, and architecture live in the **[documentation](https://dbdocs.datnguye.me/)**.
61
+
62
+ ## Why dbdocs?
63
+
64
+ dbt's own docs are great until you want lineage at the *column* level — that's the gap this fills. Everything is derived from your dbt `manifest.json` / `catalog.json` and baked into one offline-friendly SPA: catalog navigation grouped by database/schema, per-model SQL and columns, interactive React Flow graphs, column-level lineage via sqlglot, client-side search, a dark/light theme, and versioned deploys with a built-in version switcher.
65
+
66
+ See the [docs](https://dbdocs.datnguye.me/) for the deep dives.
67
+
68
+ ## Contributing
69
+
70
+ Contributions are welcome — bugs, features, docs, typos. See the **[Contributing Guide](https://dbdocs.datnguye.me/latest/nav/development/contributing-guide.html)**.
71
+
72
+ If dbdocs saves you some clicks, consider [buying me a coffee](https://www.buymeacoffee.com/datnguye).
73
+
74
+ <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>
75
+
76
+ ## License
77
+
78
+ [MIT](./LICENSE) © Dat Nguyen
dbdocs-1.0.1/README.md ADDED
@@ -0,0 +1,56 @@
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, baked into one file.</b></p>
6
+
7
+ <p align="center">
8
+ <a href="https://dbdocs.datnguye.me/latest/demo/"><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 single self-contained `index.html`: a browsable catalog, an interactive lineage DAG and ERD, and **column-level lineage** from your compiled SQL. No server, no database, no build step — just a file you can open or host anywhere.
17
+
18
+ | Catalog | Model page | Lineage DAG |
19
+ |---|---|---|
20
+ | ![catalog](docs/assets/img/demo-catalog.png) | ![model page](docs/assets/img/demo-model-page.png) | ![dag](docs/assets/img/demo-model-dag.png) |
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install dbdocs --upgrade
26
+ ```
27
+
28
+ Requires Python 3.10+.
29
+
30
+ ## Quickstart
31
+
32
+ ```bash
33
+ dbt docs generate # writes target/manifest.json + target/catalog.json
34
+ dbdocs generate # builds ./site/index.html with all data baked in
35
+ dbdocs serve # static http server on http://127.0.0.1:8000
36
+ ```
37
+
38
+ Full walkthrough, configuration, and architecture live in the **[documentation](https://dbdocs.datnguye.me/)**.
39
+
40
+ ## Why dbdocs?
41
+
42
+ dbt's own docs are great until you want lineage at the *column* level — that's the gap this fills. Everything is derived from your dbt `manifest.json` / `catalog.json` and baked into one offline-friendly SPA: catalog navigation grouped by database/schema, per-model SQL and columns, interactive React Flow graphs, column-level lineage via sqlglot, client-side search, a dark/light theme, and versioned deploys with a built-in version switcher.
43
+
44
+ See the [docs](https://dbdocs.datnguye.me/) for the deep dives.
45
+
46
+ ## Contributing
47
+
48
+ Contributions are welcome — bugs, features, docs, typos. See the **[Contributing Guide](https://dbdocs.datnguye.me/latest/nav/development/contributing-guide.html)**.
49
+
50
+ If dbdocs saves you some clicks, consider [buying me a coffee](https://www.buymeacoffee.com/datnguye).
51
+
52
+ <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>
53
+
54
+ ## License
55
+
56
+ [MIT](./LICENSE) © Dat Nguyen
@@ -1,3 +1,3 @@
1
- from dbdocs import main
2
-
3
- main.main()
1
+ from dbdocs import main
2
+
3
+ main.main()
@@ -0,0 +1,86 @@
1
+ import functools
2
+ import importlib.metadata
3
+ import socketserver
4
+ from http.server import SimpleHTTPRequestHandler
5
+
6
+ import click
7
+
8
+ from dbdocs.core.config import DbDocsConfig
9
+ from dbdocs.core.exceptions import DbDocsError
10
+ from dbdocs.core.log import logger
11
+ from dbdocs.site import deploy as deploy_module
12
+ from dbdocs.site.builder import ReportBuilder
13
+
14
+ __version__ = importlib.metadata.version("dbdocs")
15
+
16
+
17
+ # dbdocs
18
+ @click.group(
19
+ context_settings={"help_option_names": ["-h", "--help"]},
20
+ invoke_without_command=True,
21
+ no_args_is_help=True,
22
+ epilog="Specify one of these sub-commands and you can find more help from there.",
23
+ )
24
+ @click.version_option(__version__)
25
+ @click.option("-c", "--config", "config_path", default=None, help="Path to dbdocs.yml.")
26
+ @click.pass_context
27
+ def dbdocs(ctx, config_path):
28
+ """Alternative dbt docs site: dbt docs + ERD + column-level lineage."""
29
+ logger.info("Run with dbdocs==%s", __version__)
30
+ try:
31
+ ctx.obj = DbDocsConfig.load(config_path)
32
+ except DbDocsError as exc:
33
+ raise click.ClickException(str(exc)) from exc
34
+
35
+
36
+ @dbdocs.command(name="generate")
37
+ @click.option("-o", "--output-dir", default=None, help="Where to write the site (default: config).")
38
+ @click.option(
39
+ "--dialect", default=None, help="SQL dialect for column lineage (default: adapter_type)."
40
+ )
41
+ @click.pass_obj
42
+ def generate(config: DbDocsConfig, output_dir, dialect):
43
+ """Build the self-contained site from dbt artifacts."""
44
+ if dialect is not None:
45
+ config.dialect = dialect
46
+ try:
47
+ out = ReportBuilder(config).generate(output_dir=output_dir)
48
+ except DbDocsError as exc:
49
+ raise click.ClickException(str(exc)) from exc
50
+ click.echo(f"Generated site into {out}")
51
+
52
+
53
+ @dbdocs.command(name="serve")
54
+ @click.option("-p", "--port", default=8000, show_default=True, help="Port to serve on.")
55
+ @click.pass_obj
56
+ def serve(config: DbDocsConfig, port):
57
+ """Serve the generated site locally (static http server)."""
58
+ handler = functools.partial(SimpleHTTPRequestHandler, directory=config.output_path)
59
+ click.echo(f"Serving {config.output_path} at http://127.0.0.1:{port} (Ctrl-C to stop)")
60
+ socketserver.ThreadingTCPServer.allow_reuse_address = True
61
+ with socketserver.ThreadingTCPServer(("127.0.0.1", port), handler) as httpd:
62
+ httpd.serve_forever()
63
+
64
+
65
+ @dbdocs.command(name="deploy")
66
+ @click.option("--version", "version", required=True, help="Version label to deploy (e.g. 1.2).")
67
+ @click.option("--alias", default=None, help="Moving alias for this version (e.g. latest).")
68
+ @click.option(
69
+ "--title", default=None, help="Display title for this version (default: the version)."
70
+ )
71
+ @click.option(
72
+ "--delete", "delete", is_flag=True, default=False, help="Delete this version instead."
73
+ )
74
+ @click.option("--push/--no-push", default=False, help="Publish to the gh-pages branch.")
75
+ @click.pass_obj
76
+ def deploy(config: DbDocsConfig, version, alias, title, delete, push):
77
+ """Generate a versioned build and update the version index (or --delete one)."""
78
+ try:
79
+ if delete:
80
+ deploy_module.delete(config, version=version, push=push)
81
+ click.echo(f"Deleted version {version}")
82
+ return
83
+ out = deploy_module.deploy(config, version=version, alias=alias, push=push, title=title)
84
+ except DbDocsError as exc:
85
+ raise click.ClickException(str(exc)) from exc
86
+ click.echo(f"Deployed version {version} into {out}")
@@ -0,0 +1,82 @@
1
+ """Loading dbt artifacts (manifest/catalog) via the dbterd parser.
2
+
3
+ dbterd parses ``manifest.json`` / ``catalog.json`` into ``dbt_artifacts_parser``
4
+ Pydantic models. Two cross-cutting gotchas live here so the rest of dbdocs never
5
+ has to think about them:
6
+
7
+ * **Schema field aliasing.** ``dbt_artifacts_parser`` aliases the ``schema``
8
+ field to ``schema_`` to avoid clobbering Pydantic's ``BaseModel.schema()`` —
9
+ so ``node.schema`` is a *bound method*, not the value. Always read
10
+ ``node.schema_``; :func:`db_schema` centralizes that.
11
+ * **Schema-version relaxation.** Passing the detected schema version to
12
+ ``read_manifest``/``read_catalog`` makes dbterd apply its relaxation policies,
13
+ keeping parsing robust across dbt versions (including dbt Core 2.0).
14
+ """
15
+
16
+ import json
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ from dbterd.helpers import file
21
+
22
+ #: Bucket label used when a node/source has no database or schema set.
23
+ UNKNOWN = "_unknown"
24
+
25
+ #: unique_id prefixes surfaced as catalog nodes (tests/macros/etc. excluded).
26
+ NODE_PREFIXES = ("model.", "seed.", "snapshot.")
27
+
28
+
29
+ def artifact_version(target_path: str, artifact: str) -> "int | None":
30
+ """Resolve a dbt artifact's schema version int from its ``dbt_schema_version``.
31
+
32
+ Returns ``None`` (auto-detect, strict) if the version can't be determined —
33
+ e.g. the file is missing or not valid JSON.
34
+ """
35
+ artifact_path = Path(target_path) / f"{artifact}.json"
36
+ try:
37
+ metadata = json.loads(artifact_path.read_text(encoding="utf-8")).get("metadata", {})
38
+ except (OSError, json.JSONDecodeError):
39
+ return None
40
+ extracted = file.extract_artifact_version_from_file(metadata.get("dbt_schema_version", ""))
41
+ return int(extracted) if extracted else None
42
+
43
+
44
+ def load_artifacts(target_path: str) -> "tuple[Any, Any]":
45
+ """Return the dbterd-parsed ``(manifest, catalog)`` for a dbt target dir."""
46
+ manifest = file.read_manifest(
47
+ path=target_path, version=artifact_version(target_path, "manifest")
48
+ )
49
+ catalog = file.read_catalog(path=target_path, version=artifact_version(target_path, "catalog"))
50
+ return manifest, catalog
51
+
52
+
53
+ def adapter_type(target_path: str) -> "str | None":
54
+ """The warehouse adapter (``snowflake``/``bigquery``/…) from manifest metadata.
55
+
56
+ Read from the raw JSON rather than the parsed model so it works regardless of
57
+ how the parser exposes ``metadata``. Used as the default sqlglot dialect for
58
+ column-level lineage. ``None`` if unreadable.
59
+ """
60
+ manifest_path = Path(target_path) / "manifest.json"
61
+ try:
62
+ metadata = json.loads(manifest_path.read_text(encoding="utf-8")).get("metadata", {})
63
+ except (OSError, json.JSONDecodeError):
64
+ return None
65
+ return metadata.get("adapter_type")
66
+
67
+
68
+ def db_schema(entity: Any) -> "tuple[str, str]":
69
+ """The ``(database, schema)`` an entity lands in, with safe fallbacks.
70
+
71
+ Reads ``schema_`` (the Pydantic alias — ``schema`` is a bound method) and
72
+ falls back to :data:`UNKNOWN` when either part is missing, so grouping never
73
+ produces a ``None`` bucket.
74
+ """
75
+ database = getattr(entity, "database", None) or UNKNOWN
76
+ schema = getattr(entity, "schema_", None) or UNKNOWN
77
+ return str(database), str(schema)
78
+
79
+
80
+ def node_name(unique_id: str) -> str:
81
+ """The dbt node's short name — the last dotted segment of its unique_id."""
82
+ return unique_id.split(".")[-1]
@@ -0,0 +1,136 @@
1
+ from dataclasses import asdict, dataclass, field, fields
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+ from dbdocs.core.exceptions import DbDocsConfigError
7
+
8
+ DEFAULT_CONFIG_FILENAME = "dbdocs.yml"
9
+
10
+
11
+ def _resolve_within_cwd(value: str, field_name: str) -> Path:
12
+ """Resolve *value* against cwd; reject a relative path that escapes the cwd.
13
+
14
+ An absolute *value* is resolved as-is (the caller's deliberate choice — this
15
+ is documented behaviour and must not be rejected). A relative *value* is
16
+ resolved against ``Path.cwd()`` and then checked: if the result is not
17
+ inside (or equal to) the cwd, ``DbDocsConfigError`` is raised.
18
+ """
19
+ base = Path.cwd().resolve()
20
+ candidate = Path(value)
21
+ resolved = candidate if candidate.is_absolute() else (base / candidate)
22
+ resolved = resolved.resolve()
23
+ if not candidate.is_absolute() and resolved != base and base not in resolved.parents:
24
+ raise DbDocsConfigError(f"{field_name} {value!r} escapes the project directory ({base}).")
25
+ return resolved
26
+
27
+
28
+ @dataclass
29
+ class DbDocsConfig:
30
+ """Site configuration for a dbdocs build.
31
+
32
+ Loaded from a ``dbdocs.yml`` in the working directory; every field has a
33
+ default so the file is optional. ``version`` is intentionally absent — it is
34
+ a ``deploy`` CLI argument, not site config.
35
+
36
+ ``target_dir`` is where the dbt artifacts are read from; ``output_dir`` is
37
+ where the generated self-contained site is written.
38
+ """
39
+
40
+ site_name: str = "dbt docs"
41
+ site_url: str = "https://github.com/datnguye/dbt-docs"
42
+ site_author: str = "Dat Nguyen"
43
+ site_description: str = "Alternative dbt documentation site"
44
+ repo_name: str = "datnguye/dbt-docs"
45
+ repo_url: str = "https://github.com/datnguye/dbt-docs"
46
+ project_name: str = "dbt docs"
47
+ #: The footer's Buy-me-a-coffee badge shows by default; set false to hide it.
48
+ show_buy_me_a_coffee: bool = True
49
+ #: Project README rendered on the overview (relative to the working dir). Set
50
+ #: empty to omit the README section. Missing file ⇒ section simply absent.
51
+ readme: str = "README.md"
52
+ target_dir: str = "target"
53
+ #: Where the generated site is written. Nested under the dbt ``target/`` by
54
+ #: default so docs sit alongside the artifacts they're built from.
55
+ output_dir: str = "target/site"
56
+ #: SQL dialect for column-lineage parsing; ``None`` ⇒ derive from the
57
+ #: artifact's ``adapter_type`` (e.g. snowflake, bigquery, postgres).
58
+ dialect: "str | None" = None
59
+ #: Alias the SPA's version switcher treats as the default landing version.
60
+ default_version: str = "latest"
61
+ #: dbterd ERD options (``algo``, ``entity_name_format``, ``select``,
62
+ #: ``resource_type``, …) passed straight to ``DbtErd``. Configured here so the
63
+ #: ERD shape lives in ``dbdocs.yml`` rather than a separate ``.dbterd.yml``.
64
+ dbterd: dict = field(default_factory=dict)
65
+
66
+ @classmethod
67
+ def load(cls, path: "str | Path | None" = None) -> "DbDocsConfig":
68
+ """Load config from ``path`` (or ``./dbdocs.yml``); all-defaults if absent."""
69
+ config_path = Path(path) if path is not None else Path.cwd() / DEFAULT_CONFIG_FILENAME
70
+ if not config_path.is_file():
71
+ return cls()
72
+
73
+ try:
74
+ raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
75
+ except yaml.YAMLError as exc:
76
+ raise DbDocsConfigError(f"Could not parse {config_path}: {exc}") from exc
77
+
78
+ if raw is None:
79
+ return cls()
80
+ if not isinstance(raw, dict):
81
+ raise DbDocsConfigError(
82
+ f"{config_path} must contain a mapping, got {type(raw).__name__}"
83
+ )
84
+
85
+ known = {f.name for f in fields(cls)}
86
+ unknown = set(raw) - known
87
+ if unknown:
88
+ raise DbDocsConfigError(
89
+ f"Unknown keys in {config_path}: {', '.join(sorted(unknown))}. "
90
+ f"Allowed keys: {', '.join(sorted(known))}."
91
+ )
92
+ return cls(**raw)
93
+
94
+ #: Build-control fields that are not part of the site's display metadata.
95
+ _NON_METADATA_FIELDS = (
96
+ "target_dir",
97
+ "output_dir",
98
+ "dialect",
99
+ "default_version",
100
+ "dbterd",
101
+ "readme",
102
+ )
103
+
104
+ def render_context(self) -> dict:
105
+ """The site display metadata injected into the SPA's ``metadata`` block.
106
+
107
+ Excludes build-control fields (where artifacts are read, where the site
108
+ is written, the lineage dialect override) that aren't site metadata.
109
+ """
110
+ context = asdict(self)
111
+ for field_name in self._NON_METADATA_FIELDS:
112
+ context.pop(field_name, None)
113
+ return context
114
+
115
+ @property
116
+ def target_path(self) -> str:
117
+ """Absolute path to the dbt target/ dir where the artifacts live.
118
+
119
+ A relative ``target_dir`` is resolved against the current working
120
+ directory **at access time** — this is intentional and must stay aligned
121
+ with dbterd's ``DbtErd``, which also reads artifacts from ``./target``
122
+ relative to the cwd. An absolute ``target_dir`` is returned unchanged.
123
+ A relative path that would escape the cwd raises ``DbDocsConfigError``.
124
+ """
125
+ return str(_resolve_within_cwd(self.target_dir, "target_dir"))
126
+
127
+ @property
128
+ def output_path(self) -> str:
129
+ """Absolute path to the dir the generated site is written into.
130
+
131
+ Resolved against the cwd at access time, mirroring ``target_path`` — a
132
+ relative ``output_dir`` follows the working directory, an absolute one is
133
+ returned unchanged.
134
+ A relative path that would escape the cwd raises ``DbDocsConfigError``.
135
+ """
136
+ return str(_resolve_within_cwd(self.output_dir, "output_dir"))