quickdbd-cli 0.1.1__tar.gz → 0.1.3__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 (50) hide show
  1. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/PKG-INFO +24 -2
  2. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/README.md +23 -1
  3. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/pyproject.toml +1 -1
  4. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/scripts/release.sh +7 -2
  5. quickdbd_cli-0.1.3/src/quickdbd_cli/__init__.py +1 -0
  6. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/render.js +16 -5
  7. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/style.css +9 -0
  8. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/viewer.js +45 -2
  9. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/cli.py +59 -0
  10. quickdbd_cli-0.1.3/src/quickdbd_cli/dbt.py +412 -0
  11. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/html.py +1 -0
  12. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/measure.py +11 -4
  13. quickdbd_cli-0.1.3/tests/fixtures/jaffle/LICENSE +201 -0
  14. quickdbd_cli-0.1.3/tests/fixtures/jaffle/README.md +2 -0
  15. quickdbd_cli-0.1.3/tests/fixtures/jaffle/catalog.json +227 -0
  16. quickdbd_cli-0.1.3/tests/fixtures/jaffle/manifest.json +880 -0
  17. quickdbd_cli-0.1.3/tests/test_dbt.py +231 -0
  18. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/tests/test_pipeline.py +31 -0
  19. quickdbd_cli-0.1.1/src/quickdbd_cli/__init__.py +0 -1
  20. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/.gitignore +0 -0
  21. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/LICENSE +0 -0
  22. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/NOTICE +0 -0
  23. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/examples/quickdbd-sample.txt +0 -0
  24. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/examples/synthetic/finance.txt +0 -0
  25. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/examples/synthetic/hr.txt +0 -0
  26. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/examples/synthetic/inventory.txt +0 -0
  27. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/examples/synthetic/marketing.txt +0 -0
  28. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/examples/synthetic/qdbd.layout.json +0 -0
  29. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/examples/synthetic/sales.txt +0 -0
  30. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/scripts/gen_font_metrics.py +0 -0
  31. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/scripts/gen_schema.py +0 -0
  32. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/__main__.py +0 -0
  33. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  34. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  35. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  36. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  37. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  38. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/editor.js +0 -0
  39. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  40. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  41. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  42. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/export.py +0 -0
  43. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/jsrt.py +0 -0
  44. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/layout.py +0 -0
  45. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/model.py +0 -0
  46. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/parser.py +0 -0
  47. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/project.py +0 -0
  48. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/src/quickdbd_cli/serve.py +0 -0
  49. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/tests/test_parser.py +0 -0
  50. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.3}/tests/test_serve.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit.
5
5
  Project-URL: Homepage, https://github.com/MarkPhamm/quickdbd-cli
6
6
  Author: Minh (Mark) Pham
@@ -55,12 +55,33 @@ Not affiliated with QuickDBD. It reads the same text syntax, so schemas move fre
55
55
  - click a table to focus on it and its neighbours
56
56
  - collapse tables: full / keys only / names only
57
57
  - hover a column to highlight its relationship
58
+ - hover a dotted-underlined table or column to read its `#` doc comments
58
59
  - PNG/SVG export buttons
59
60
  - **Live editing:** `qdbd serve` reloads when you save, has a browser editor pane with autosave, and remembers where you drag tables.
60
61
  - **Many files, one diagram:** point it at a folder and foreign keys can cross files.
61
62
  - **Pip-only install:** no Graphviz, Node or browser needed. Layout uses [ELK](https://eclipse.dev/elk/), running in an embedded V8.
62
63
  - **CI-friendly:** builds are deterministic, and errors are `file:line: error: …` with exit code 1.
63
64
 
65
+ ## From a dbt project
66
+
67
+ ```bash
68
+ dbt docs generate # optional: adds real column types (catalog.json)
69
+ qdbd import dbt target/manifest.json -o erd/ # one .txt per folder (staging, marts, sources, ...)
70
+ qdbd build erd/ -o erd.html
71
+ ```
72
+
73
+ - **Tables:** models, seeds and snapshots of your project, plus sources.
74
+ - **Relationships:** `relationships` tests (including dbt_utils `relationships_where`) and `foreign_key` constraints.
75
+ - **Primary keys:** `primary_key` constraints, or else a column with both `unique` and `not_null` tests.
76
+ - **Docs:** model and column descriptions become doc comments, shown as tooltips.
77
+ - **Options:**
78
+ - `--select 'marts.*'` narrows by name, fqn or path (repeatable)
79
+ - `--package all` includes installed packages
80
+ - `--no-sources` drops sources
81
+ - `-o schema.txt` writes a single file
82
+
83
+ Re-running the import overwrites the generated files. Table positions survive because they live in the layout file, keyed by table name.
84
+
64
85
  ## Commands
65
86
 
66
87
  ```bash
@@ -69,6 +90,7 @@ qdbd build PATH... -o diagram.html # shareable interactive HTML (pos
69
90
  qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
70
91
  qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG or SVG image
71
92
  qdbd layout PATH... [--reset] # (re)write the layout file
93
+ qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
72
94
  ```
73
95
 
74
96
  `PATH` is a `.txt` file or a folder. Every `*.txt` under a folder is read recursively.
@@ -157,7 +179,7 @@ TWINE_USERNAME=__token__
157
179
  TWINE_PASSWORD=pypi-...
158
180
  ```
159
181
 
160
- Bump `version` in `pyproject.toml` and `src/quickdbd_cli/__init__.py`, commit, then run `scripts/release.sh`. It builds, uploads to PyPI, tags `vX.Y.Z` and creates the GitHub release.
182
+ Bump `version` in `pyproject.toml` and `src/quickdbd_cli/__init__.py`, commit, then run `PATH="$PWD/.venv/bin:$PATH" scripts/release.sh`. It builds, uploads to PyPI, tags `vX.Y.Z` and creates the GitHub release.
161
183
 
162
184
  ## License
163
185
 
@@ -30,12 +30,33 @@ Not affiliated with QuickDBD. It reads the same text syntax, so schemas move fre
30
30
  - click a table to focus on it and its neighbours
31
31
  - collapse tables: full / keys only / names only
32
32
  - hover a column to highlight its relationship
33
+ - hover a dotted-underlined table or column to read its `#` doc comments
33
34
  - PNG/SVG export buttons
34
35
  - **Live editing:** `qdbd serve` reloads when you save, has a browser editor pane with autosave, and remembers where you drag tables.
35
36
  - **Many files, one diagram:** point it at a folder and foreign keys can cross files.
36
37
  - **Pip-only install:** no Graphviz, Node or browser needed. Layout uses [ELK](https://eclipse.dev/elk/), running in an embedded V8.
37
38
  - **CI-friendly:** builds are deterministic, and errors are `file:line: error: …` with exit code 1.
38
39
 
40
+ ## From a dbt project
41
+
42
+ ```bash
43
+ dbt docs generate # optional: adds real column types (catalog.json)
44
+ qdbd import dbt target/manifest.json -o erd/ # one .txt per folder (staging, marts, sources, ...)
45
+ qdbd build erd/ -o erd.html
46
+ ```
47
+
48
+ - **Tables:** models, seeds and snapshots of your project, plus sources.
49
+ - **Relationships:** `relationships` tests (including dbt_utils `relationships_where`) and `foreign_key` constraints.
50
+ - **Primary keys:** `primary_key` constraints, or else a column with both `unique` and `not_null` tests.
51
+ - **Docs:** model and column descriptions become doc comments, shown as tooltips.
52
+ - **Options:**
53
+ - `--select 'marts.*'` narrows by name, fqn or path (repeatable)
54
+ - `--package all` includes installed packages
55
+ - `--no-sources` drops sources
56
+ - `-o schema.txt` writes a single file
57
+
58
+ Re-running the import overwrites the generated files. Table positions survive because they live in the layout file, keyed by table name.
59
+
39
60
  ## Commands
40
61
 
41
62
  ```bash
@@ -44,6 +65,7 @@ qdbd build PATH... -o diagram.html # shareable interactive HTML (pos
44
65
  qdbd serve PATH... [--port 8765] # live editor + drag-to-arrange
45
66
  qdbd export PATH... -o out.png [--focus Customer --depth 1] # PNG or SVG image
46
67
  qdbd layout PATH... [--reset] # (re)write the layout file
68
+ qdbd import dbt target/manifest.json -o erd/ # generate schema files from a dbt project
47
69
  ```
48
70
 
49
71
  `PATH` is a `.txt` file or a folder. Every `*.txt` under a folder is read recursively.
@@ -132,7 +154,7 @@ TWINE_USERNAME=__token__
132
154
  TWINE_PASSWORD=pypi-...
133
155
  ```
134
156
 
135
- Bump `version` in `pyproject.toml` and `src/quickdbd_cli/__init__.py`, commit, then run `scripts/release.sh`. It builds, uploads to PyPI, tags `vX.Y.Z` and creates the GitHub release.
157
+ Bump `version` in `pyproject.toml` and `src/quickdbd_cli/__init__.py`, commit, then run `PATH="$PWD/.venv/bin:$PATH" scripts/release.sh`. It builds, uploads to PyPI, tags `vX.Y.Z` and creates the GitHub release.
136
158
 
137
159
  ## License
138
160
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quickdbd-cli"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # Build and upload the current version to PyPI using credentials from .env,
3
3
  # then tag and create the GitHub release.
4
- # Usage: bump `version` in pyproject.toml and src/quickdbd_cli/__init__.py, commit, then run scripts/release.sh
4
+ # Usage: bump `version` in pyproject.toml and src/quickdbd_cli/__init__.py, commit, then run
5
+ # PATH="$PWD/.venv/bin:$PATH" scripts/release.sh
5
6
  set -euo pipefail
6
7
  cd "$(dirname "$0")/.."
7
8
 
@@ -12,7 +13,11 @@ version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','
12
13
  [ -z "$(git status --porcelain --untracked-files=no)" ] || { echo "error: uncommitted changes" >&2; exit 1; }
13
14
 
14
15
  rm -rf dist
15
- python3 -m pip install -q --upgrade build twine
16
+ # uv-created venvs have no pip; only install the tools when they're missing.
17
+ python3 -c "import build, twine" 2>/dev/null || {
18
+ if command -v uv >/dev/null; then uv pip install -q --python "$(command -v python3)" build twine
19
+ else python3 -m pip install -q --upgrade build twine; fi
20
+ }
16
21
  python3 -m build
17
22
  python3 -m twine check dist/*
18
23
  python3 -m twine upload --non-interactive dist/*
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -66,13 +66,22 @@
66
66
  '<path d="M' + n(x + 6.5) + " " + n(cy) + "h7m-2 0v3m-2.5-3v2" + '"/></g>';
67
67
  }
68
68
 
69
- function renderTable(model, t, g, p) {
69
+ /* Dotted underline marking text that has a doc comment (interactive viewer only). */
70
+ function docMark(x, y, w, color) {
71
+ return '<line class="dm" x1="' + n(x) + '" y1="' + n(y) + '" x2="' + n(x + w) + '" y2="' + n(y) +
72
+ '" stroke="' + color + '" stroke-width="1" stroke-dasharray="1.5 2"/>';
73
+ }
74
+
75
+ function renderTable(model, t, g, p, docMarks) {
70
76
  var st = model.style, w = g.w, h = g.h, out = [];
71
77
  out.push('<g class="t" data-id="' + esc(t.id) + '" transform="translate(' + n(p[0]) + "," + n(p[1]) + ')">');
72
78
  out.push('<rect class="tb" width="' + w + '" height="' + h + '" rx="2" fill="' + C.body + '" stroke="' + C.border + '"/>');
73
79
  out.push('<path class="th" d="M0 2a2 2 0 0 1 2-2h' + (w - 4) + "a2 2 0 0 1 2 2v" + (st.headerH - 2) + "h-" + w + 'z" fill="' + C.header + '"/>');
74
80
  out.push('<text x="' + st.padX + '" y="' + n(st.headerH / 2 + st.headerSize * 0.36) + '" fill="' + C.headerText +
75
81
  '" font-family="' + FONT + '" font-size="' + st.headerSize + '">' + esc(t.name) + "</text>");
82
+ if (docMarks && t.doc) {
83
+ out.push(docMark(st.padX, st.headerH / 2 + st.headerSize * 0.36 + 3, t.nameW, "rgba(255,255,255,.7)"));
84
+ }
76
85
  g.cols.forEach(function (c, i) {
77
86
  var y = st.headerH + i * st.rowH, cy = y + st.rowH / 2, base = n(cy + st.rowSize * 0.36);
78
87
  var weight = c.pk || c.fk ? ' font-weight="bold"' : "";
@@ -81,6 +90,7 @@
81
90
  out.push('<rect class="rb" y="' + y + '" width="' + w + '" height="' + st.rowH + '" fill="' + C.body + '" fill-opacity="0"/>');
82
91
  out.push('<text x="' + st.padX + '" y="' + base + '" fill="' + color + '" font-family="' + FONT + '" font-size="' +
83
92
  st.rowSize + '"' + weight + ">" + esc(c.name) + "</text>");
93
+ if (docMarks && c.doc) out.push(docMark(st.padX, cy + st.rowSize * 0.36 + 2.5, c.nameW, color));
84
94
  var bx = st.padX + c.nameW + 6;
85
95
  c.badges.forEach(function (b) {
86
96
  var bh = st.badgeSize + 5;
@@ -89,7 +99,7 @@
89
99
  '" font-family="' + FONT + '" font-size="' + st.badgeSize + '">' + esc(b.t) + "</text>");
90
100
  bx += b.w + st.badgeGap;
91
101
  });
92
- if (c.pk) out.push(keyIcon(w - st.padX - c.typeW - st.keyW + 1, cy));
102
+ if (c.pk) out.push(keyIcon(w - st.padX - c.typeW - st.keyW, cy));
93
103
  out.push('<text x="' + (w - st.padX) + '" y="' + base + '" text-anchor="end" fill="' + C.type + '" font-family="' + FONT +
94
104
  '" font-size="' + st.rowSize + '">' + esc(c.type) + "</text>");
95
105
  out.push("</g>");
@@ -164,9 +174,10 @@
164
174
  return model.rels.map(function (r) { return renderEdge(model, geo, pos, r); }).join("");
165
175
  }
166
176
 
167
- function renderInner(model, pos, mode) {
168
- var geo = geometry(model, mode);
169
- var tables = model.tables.map(function (t) { return pos[t.id] ? renderTable(model, t, geo[t.id], pos[t.id]) : ""; }).join("");
177
+ /* opts.docMarks: underline tables/columns that have doc comments (viewer only, not exports). */
178
+ function renderInner(model, pos, mode, opts) {
179
+ var geo = geometry(model, mode), marks = !!(opts && opts.docMarks);
180
+ var tables = model.tables.map(function (t) { return pos[t.id] ? renderTable(model, t, geo[t.id], pos[t.id], marks) : ""; }).join("");
170
181
  return '<g class="edges">' + renderEdges(model, pos, mode, geo) + '</g><g class="tables">' + tables + "</g>";
171
182
  }
172
183
 
@@ -53,6 +53,15 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
53
53
  .minimap svg { width: 100%; height: 100%; display: block; }
54
54
  .minimap .vpr { fill: rgba(43,87,151,.12); stroke: var(--blue); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
55
55
 
56
+ /* doc-comment tooltip */
57
+ .tip { position: absolute; display: none; max-width: 340px; background: #23272b; color: #eee; border-radius: 6px;
58
+ padding: 8px 10px; font-size: 12px; line-height: 1.45; pointer-events: none; box-shadow: 0 4px 14px rgba(0,0,0,.25);
59
+ z-index: 5; }
60
+ .tip.open { display: block; }
61
+ .tip .tt { font-weight: bold; margin-bottom: 3px; }
62
+ .tip .tt span { font-weight: normal; color: #9aa4ad; margin-left: 8px; }
63
+ .tip .td { white-space: pre-wrap; color: #d8dde2; }
64
+
56
65
  .hint { position: absolute; left: 16px; bottom: 14px; color: var(--muted); font-size: 12px; }
57
66
 
58
67
  /* serve: editor pane */
@@ -24,7 +24,7 @@
24
24
 
25
25
  /* ------------------------------------------------------------- render */
26
26
  function render() {
27
- vp.innerHTML = Q.renderInner(S.model, S.pos, S.mode);
27
+ vp.innerHTML = Q.renderInner(S.model, S.pos, S.mode, { docMarks: true });
28
28
  applyFocus();
29
29
  drawMinimap();
30
30
  }
@@ -124,7 +124,50 @@
124
124
  if (t) t.classList.add("hl");
125
125
  });
126
126
  });
127
- vp.addEventListener("mouseleave", clearHover);
127
+ vp.addEventListener("mouseleave", function () { clearHover(); hideTip(); });
128
+
129
+ /* ------------------------------------------------------------- doc-comment tooltips */
130
+ var tip = document.getElementById("tip");
131
+ function hideTip() { tip.classList.remove("open"); }
132
+ function showTip(ev, title, sub, doc) {
133
+ tip.innerHTML = "";
134
+ var h = document.createElement("div");
135
+ h.className = "tt";
136
+ h.textContent = title;
137
+ if (sub) {
138
+ var s = document.createElement("span");
139
+ s.textContent = sub;
140
+ h.appendChild(s);
141
+ }
142
+ var d = document.createElement("div");
143
+ d.className = "td";
144
+ d.textContent = doc;
145
+ tip.appendChild(h);
146
+ tip.appendChild(d);
147
+ tip.classList.add("open");
148
+ moveTip(ev);
149
+ }
150
+ function moveTip(ev) {
151
+ var r = svg.getBoundingClientRect(), x = ev.clientX - r.left + 14, y = ev.clientY - r.top + 14;
152
+ var w = tip.offsetWidth, h = tip.offsetHeight;
153
+ if (x + w > r.width - 8) x = ev.clientX - r.left - w - 14;
154
+ if (y + h > r.height - 8) y = ev.clientY - r.top - h - 14;
155
+ tip.style.left = Math.max(8, x) + "px";
156
+ tip.style.top = Math.max(8, y) + "px";
157
+ }
158
+ vp.addEventListener("mousemove", function (ev) {
159
+ if (drag && drag.moved) { hideTip(); return; }
160
+ var tEl = ev.target.closest && ev.target.closest(".t");
161
+ if (!tEl) { hideTip(); return; }
162
+ var t = byId[tEl.getAttribute("data-id")], row = ev.target.closest(".r");
163
+ if (row) {
164
+ var cid = row.getAttribute("data-c"), c = t.cols.filter(function (x) { return x.id === cid; })[0];
165
+ if (c && c.doc) showTip(ev, t.name + "." + c.name, c.type, c.doc);
166
+ else hideTip();
167
+ } else if (t.doc && ev.target.closest(".th, text")) {
168
+ showTip(ev, t.name, t.aliases.length ? "as " + t.aliases.join(", ") : "", t.doc);
169
+ } else hideTip();
170
+ });
128
171
 
129
172
  /* ------------------------------------------------------------- pointer: pan, click, drag */
130
173
  var drag = null;
@@ -103,6 +103,43 @@ def cmd_layout(args) -> int:
103
103
  return 0
104
104
 
105
105
 
106
+ def cmd_import_dbt(args) -> int:
107
+ from .dbt import DbtImportError, import_dbt, write_result
108
+ from .parser import compile_sources
109
+
110
+ out = Path(args.output)
111
+ split = out.suffix != ".txt"
112
+ try:
113
+ result = import_dbt(
114
+ Path(args.manifest),
115
+ Path(args.catalog) if args.catalog else None,
116
+ packages=args.package,
117
+ select=args.select,
118
+ sources=not args.no_sources,
119
+ split=split,
120
+ )
121
+ except DbtImportError as e:
122
+ print(f"error: {e}", file=sys.stderr)
123
+ return 1
124
+ # Self-check: the generated text must compile cleanly.
125
+ schema = compile_sources([(name, text) for name, text in result.files.items()])
126
+ if schema.errors:
127
+ for d in schema.errors:
128
+ print(d.format(), file=sys.stderr)
129
+ print("error: generated schema did not compile (please report this as a bug)", file=sys.stderr)
130
+ return 1
131
+ written = write_result(result, out)
132
+ for s in result.skipped:
133
+ print(f"skipped: {s}", file=sys.stderr)
134
+ where = str(out) if not split else f"{out}/ ({len(written)} files)"
135
+ print(f"Wrote {where}: {result.tables} tables, {result.relationships} relationships")
136
+ if result.tables and not result.relationships:
137
+ print(
138
+ "hint: no relationships found; add `relationships` tests or foreign_key constraints in dbt", file=sys.stderr
139
+ )
140
+ return 0
141
+
142
+
106
143
  def cmd_serve(args) -> int:
107
144
  from .serve import serve
108
145
 
@@ -147,6 +184,28 @@ def build_parser() -> argparse.ArgumentParser:
147
184
  sp.add_argument("--layout")
148
185
  sp.set_defaults(func=cmd_layout)
149
186
 
187
+ sp = sub.add_parser("import", help="generate QuickDBD schema files from another source")
188
+ isub = sp.add_subparsers(dest="source", required=True)
189
+ dp = isub.add_parser(
190
+ "dbt",
191
+ help="from a dbt project's target/manifest.json",
192
+ description="Tables from models/seeds/snapshots/sources, relationships from `relationships` tests and "
193
+ "foreign_key constraints, types from catalog.json (run `dbt docs generate` first for types).",
194
+ )
195
+ dp.add_argument("manifest", help="path to manifest.json (or the target/ directory)")
196
+ dp.add_argument("-o", "--output", default="schema.txt", help="a .txt file, or a directory for one file per folder")
197
+ dp.add_argument("--catalog", help="catalog.json for column types (default: next to the manifest, if present)")
198
+ dp.add_argument(
199
+ "--package",
200
+ action="append",
201
+ help="dbt package(s) to include (default: the root project; 'all' for every package)",
202
+ )
203
+ dp.add_argument(
204
+ "--select", action="append", help="glob on model name, fqn (a.b.c) or file path, e.g. 'marts.*' (repeatable)"
205
+ )
206
+ dp.add_argument("--no-sources", action="store_true", help="leave out dbt sources")
207
+ dp.set_defaults(func=cmd_import_dbt)
208
+
150
209
  sp = sub.add_parser("serve", help="live editor: reload on save, drag tables to arrange")
151
210
  paths(sp)
152
211
  sp.add_argument("--port", type=int, default=8765)