quickdbd-cli 0.1.7__tar.gz → 0.1.9__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.
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/PKG-INFO +54 -7
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/README.md +53 -6
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/pyproject.toml +1 -1
- quickdbd_cli-0.1.9/src/quickdbd_cli/__init__.py +1 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/render.js +5 -4
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/style.css +11 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/viewer.js +29 -2
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/cli.py +50 -3
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/dbt.py +15 -1
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/html.py +24 -7
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/measure.py +10 -4
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/model.py +3 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/parser.py +48 -1
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/serve.py +27 -11
- quickdbd_cli-0.1.9/src/quickdbd_cli/share.py +109 -0
- quickdbd_cli-0.1.9/src/quickdbd_cli/tags.py +95 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/test_select.py +3 -3
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/test_serve.py +36 -0
- quickdbd_cli-0.1.9/tests/test_share.py +116 -0
- quickdbd_cli-0.1.9/tests/test_tags.py +196 -0
- quickdbd_cli-0.1.7/src/quickdbd_cli/__init__.py +0 -1
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/.gitignore +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/LICENSE +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/NOTICE +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/examples/quickdbd-sample.txt +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/examples/synthetic/finance.txt +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/examples/synthetic/hr.txt +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/examples/synthetic/inventory.txt +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/examples/synthetic/marketing.txt +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/examples/synthetic/qdbd.layout.json +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/examples/synthetic/sales.txt +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/scripts/gen_font_metrics.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/scripts/gen_schema.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/scripts/release.sh +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/__main__.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/editor.js +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/assets/font-metrics.json +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/diff.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/export.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/jsrt.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/layout.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/src/quickdbd_cli/project.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/fixtures/jaffle/LICENSE +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/fixtures/jaffle/README.md +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/fixtures/jaffle/catalog.json +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/fixtures/jaffle/manifest.json +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/test_dbt.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/test_diff.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/test_parser.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/test_path.py +0 -0
- {quickdbd_cli-0.1.7 → quickdbd_cli-0.1.9}/tests/test_pipeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: quickdbd-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
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
|
|
@@ -121,7 +121,7 @@ A browser tab opens with the diagram and an editor pane.
|
|
|
121
121
|
qdbd build schema.txt -o schema.html
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
`schema.html` is a single self-contained file. Open it locally, email it, attach it to a ticket, or publish it on GitHub Pages ([7.1](#71-github-action)).
|
|
124
|
+
`schema.html` is a single self-contained file. Open it locally, email it, attach it to a ticket, or publish it on GitHub Pages ([7.1](#71-github-action)). To send a link instead of the file, see [4.4](#44-share-a-link).
|
|
125
125
|
|
|
126
126
|
### 2.5 Export an image
|
|
127
127
|
|
|
@@ -202,7 +202,32 @@ Where the layout file goes:
|
|
|
202
202
|
|
|
203
203
|
`qdbd build` never writes the layout file, so CI builds leave your working tree clean. More: [Table Positions](https://markphamm.github.io/quickdbd-cli/layout/).
|
|
204
204
|
|
|
205
|
-
### 3.4
|
|
205
|
+
### 3.4 Tags and colours
|
|
206
|
+
|
|
207
|
+
Give tables a role and a header colour. Tags are comments, so the file still pastes into QuickDBD.
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
# @color gold #b8860b # optional: pick a tag's colour
|
|
211
|
+
|
|
212
|
+
# @tag: gold, pii
|
|
213
|
+
customers
|
|
214
|
+
-
|
|
215
|
+
customer_id PK int
|
|
216
|
+
|
|
217
|
+
dim_date # tagged "dim" automatically from its name
|
|
218
|
+
-
|
|
219
|
+
date_key PK int
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
A table's tags come from the first source that has any:
|
|
223
|
+
|
|
224
|
+
1. an explicit `# @tag:` comment;
|
|
225
|
+
2. its name: `dim_*` → dim, `fct_*` / `fact_*` → fact, `stg_*` → staging, `int_*` → intermediate, `raw_*` / `src_*` → raw;
|
|
226
|
+
3. dbt tags, written as `# @dbt-tags:` by `qdbd import dbt`.
|
|
227
|
+
|
|
228
|
+
The header takes the colour of the first tag. In the page, a chip per tag under the filter box shows only that tag, and the selector `tag:fact` works everywhere. More: [Tags and Colours](https://markphamm.github.io/quickdbd-cli/tags/).
|
|
229
|
+
|
|
230
|
+
### 3.5 Differences from QuickDBD
|
|
206
231
|
|
|
207
232
|
`qdbd` accepts a few things QuickDBD rejects, and prints a warning for each:
|
|
208
233
|
|
|
@@ -243,6 +268,7 @@ The explorer's filter box takes dbt-style selectors that follow **relationships*
|
|
|
243
268
|
| `+orders+1` | one hop in both directions |
|
|
244
269
|
| `group:marts`, `file:core.txt` | a folder or a file |
|
|
245
270
|
| `col:customer_id` | tables with a matching column |
|
|
271
|
+
| `tag:fact` | tables with a matching tag ([3.4](#34-tags-and-colours)) |
|
|
246
272
|
| `a b` / `a,b` / `!a` | union / intersection / exclude |
|
|
247
273
|
|
|
248
274
|
```bash
|
|
@@ -264,6 +290,25 @@ qdbd path schema/ --from Invoice --to Campaign
|
|
|
264
290
|
|
|
265
291
|

|
|
266
292
|
|
|
293
|
+
### 4.4 Share a link
|
|
294
|
+
|
|
295
|
+
`qdbd serve`'s address only works on your machine. To send someone a **link**:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
qdbd share schema.txt # needs the GitHub CLI: brew install gh && gh auth login
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```text
|
|
302
|
+
Shared 12 tables (secret gist https://gist.github.com/you/1a2b3c…)
|
|
303
|
+
Link: https://gistpreview.github.io/?1a2b3c…
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
- The link opens the full interactive page. Running `qdbd share` again updates the same link.
|
|
307
|
+
- Gists are secret (link-only) by default; `--public` lists it.
|
|
308
|
+
- For a **live** view while you edit, run `qdbd serve schema.txt --read-only` (no editor, nothing written to disk) behind a tunnel such as `cloudflared tunnel --url http://127.0.0.1:8765`. Never tunnel a plain `qdbd serve`: viewers could edit your files.
|
|
309
|
+
|
|
310
|
+
More: [Sharing a Link](https://markphamm.github.io/quickdbd-cli/sharing/).
|
|
311
|
+
|
|
267
312
|
## 5. dbt projects
|
|
268
313
|
|
|
269
314
|
### 5.1 Generate the schema from dbt
|
|
@@ -288,6 +333,7 @@ See it on a real project: the [jaffle_shop demo](https://markphamm.github.io/qui
|
|
|
288
333
|
| Relationships | `relationships` tests (incl. dbt_utils `relationships_where`) and `foreign_key` constraints |
|
|
289
334
|
| Column types | `catalog.json` |
|
|
290
335
|
| Tooltips | model and column descriptions |
|
|
336
|
+
| Header colours | dbt `tags` (unless the name or an explicit `# @tag` says otherwise) |
|
|
291
337
|
|
|
292
338
|
Useful options: `--select 'marts.*'` (repeatable), `--no-sources`, `--package all`, `-o schema.txt` for a single file.
|
|
293
339
|
|
|
@@ -341,7 +387,7 @@ jobs:
|
|
|
341
387
|
permissions: { contents: read, pull-requests: write }
|
|
342
388
|
steps:
|
|
343
389
|
- uses: actions/checkout@v4
|
|
344
|
-
- uses: MarkPhamm/quickdbd-cli@v0.1.
|
|
390
|
+
- uses: MarkPhamm/quickdbd-cli@v0.1.9
|
|
345
391
|
with:
|
|
346
392
|
path: erd/ # schema file(s) or folder(s)
|
|
347
393
|
output: site/index.html
|
|
@@ -364,7 +410,7 @@ Run your dbt setup first, then let the action generate the schema:
|
|
|
364
410
|
|
|
365
411
|
```yaml
|
|
366
412
|
# … dbt deps && dbt build && dbt docs generate
|
|
367
|
-
- uses: MarkPhamm/quickdbd-cli@v0.1.
|
|
413
|
+
- uses: MarkPhamm/quickdbd-cli@v0.1.9
|
|
368
414
|
with:
|
|
369
415
|
dbt-manifest: target/
|
|
370
416
|
dbt-args: --infer-fks
|
|
@@ -379,7 +425,7 @@ Commit `erd/` so pull requests can be compared with `main`. Set `dbt-check: true
|
|
|
379
425
|
```yaml
|
|
380
426
|
repos:
|
|
381
427
|
- repo: https://github.com/MarkPhamm/quickdbd-cli
|
|
382
|
-
rev: v0.1.
|
|
428
|
+
rev: v0.1.9
|
|
383
429
|
hooks:
|
|
384
430
|
- id: qdbd-check
|
|
385
431
|
files: ^erd/.*\.txt$
|
|
@@ -404,8 +450,9 @@ Builds are deterministic (no timestamps or randomness), so the output only chang
|
|
|
404
450
|
|
|
405
451
|
```bash
|
|
406
452
|
qdbd check PATH... [--strict] # validate
|
|
407
|
-
qdbd serve PATH... [--port 8765]
|
|
453
|
+
qdbd serve PATH... [--port 8765] [--read-only] # live editor + drag to arrange
|
|
408
454
|
qdbd build PATH... -o diagram.html # interactive HTML
|
|
455
|
+
qdbd share PATH... [--public] # publish as a gist, print a link
|
|
409
456
|
qdbd export PATH... -o out.png|.svg|.drawio [--focus T --depth 1]
|
|
410
457
|
qdbd layout PATH... [--reset] # (re)write the layout file
|
|
411
458
|
qdbd path PATH... --from A --to B # shortest join path + SQL JOIN
|
|
@@ -93,7 +93,7 @@ A browser tab opens with the diagram and an editor pane.
|
|
|
93
93
|
qdbd build schema.txt -o schema.html
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
`schema.html` is a single self-contained file. Open it locally, email it, attach it to a ticket, or publish it on GitHub Pages ([7.1](#71-github-action)).
|
|
96
|
+
`schema.html` is a single self-contained file. Open it locally, email it, attach it to a ticket, or publish it on GitHub Pages ([7.1](#71-github-action)). To send a link instead of the file, see [4.4](#44-share-a-link).
|
|
97
97
|
|
|
98
98
|
### 2.5 Export an image
|
|
99
99
|
|
|
@@ -174,7 +174,32 @@ Where the layout file goes:
|
|
|
174
174
|
|
|
175
175
|
`qdbd build` never writes the layout file, so CI builds leave your working tree clean. More: [Table Positions](https://markphamm.github.io/quickdbd-cli/layout/).
|
|
176
176
|
|
|
177
|
-
### 3.4
|
|
177
|
+
### 3.4 Tags and colours
|
|
178
|
+
|
|
179
|
+
Give tables a role and a header colour. Tags are comments, so the file still pastes into QuickDBD.
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
# @color gold #b8860b # optional: pick a tag's colour
|
|
183
|
+
|
|
184
|
+
# @tag: gold, pii
|
|
185
|
+
customers
|
|
186
|
+
-
|
|
187
|
+
customer_id PK int
|
|
188
|
+
|
|
189
|
+
dim_date # tagged "dim" automatically from its name
|
|
190
|
+
-
|
|
191
|
+
date_key PK int
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
A table's tags come from the first source that has any:
|
|
195
|
+
|
|
196
|
+
1. an explicit `# @tag:` comment;
|
|
197
|
+
2. its name: `dim_*` → dim, `fct_*` / `fact_*` → fact, `stg_*` → staging, `int_*` → intermediate, `raw_*` / `src_*` → raw;
|
|
198
|
+
3. dbt tags, written as `# @dbt-tags:` by `qdbd import dbt`.
|
|
199
|
+
|
|
200
|
+
The header takes the colour of the first tag. In the page, a chip per tag under the filter box shows only that tag, and the selector `tag:fact` works everywhere. More: [Tags and Colours](https://markphamm.github.io/quickdbd-cli/tags/).
|
|
201
|
+
|
|
202
|
+
### 3.5 Differences from QuickDBD
|
|
178
203
|
|
|
179
204
|
`qdbd` accepts a few things QuickDBD rejects, and prints a warning for each:
|
|
180
205
|
|
|
@@ -215,6 +240,7 @@ The explorer's filter box takes dbt-style selectors that follow **relationships*
|
|
|
215
240
|
| `+orders+1` | one hop in both directions |
|
|
216
241
|
| `group:marts`, `file:core.txt` | a folder or a file |
|
|
217
242
|
| `col:customer_id` | tables with a matching column |
|
|
243
|
+
| `tag:fact` | tables with a matching tag ([3.4](#34-tags-and-colours)) |
|
|
218
244
|
| `a b` / `a,b` / `!a` | union / intersection / exclude |
|
|
219
245
|
|
|
220
246
|
```bash
|
|
@@ -236,6 +262,25 @@ qdbd path schema/ --from Invoice --to Campaign
|
|
|
236
262
|
|
|
237
263
|

|
|
238
264
|
|
|
265
|
+
### 4.4 Share a link
|
|
266
|
+
|
|
267
|
+
`qdbd serve`'s address only works on your machine. To send someone a **link**:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
qdbd share schema.txt # needs the GitHub CLI: brew install gh && gh auth login
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
```text
|
|
274
|
+
Shared 12 tables (secret gist https://gist.github.com/you/1a2b3c…)
|
|
275
|
+
Link: https://gistpreview.github.io/?1a2b3c…
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
- The link opens the full interactive page. Running `qdbd share` again updates the same link.
|
|
279
|
+
- Gists are secret (link-only) by default; `--public` lists it.
|
|
280
|
+
- For a **live** view while you edit, run `qdbd serve schema.txt --read-only` (no editor, nothing written to disk) behind a tunnel such as `cloudflared tunnel --url http://127.0.0.1:8765`. Never tunnel a plain `qdbd serve`: viewers could edit your files.
|
|
281
|
+
|
|
282
|
+
More: [Sharing a Link](https://markphamm.github.io/quickdbd-cli/sharing/).
|
|
283
|
+
|
|
239
284
|
## 5. dbt projects
|
|
240
285
|
|
|
241
286
|
### 5.1 Generate the schema from dbt
|
|
@@ -260,6 +305,7 @@ See it on a real project: the [jaffle_shop demo](https://markphamm.github.io/qui
|
|
|
260
305
|
| Relationships | `relationships` tests (incl. dbt_utils `relationships_where`) and `foreign_key` constraints |
|
|
261
306
|
| Column types | `catalog.json` |
|
|
262
307
|
| Tooltips | model and column descriptions |
|
|
308
|
+
| Header colours | dbt `tags` (unless the name or an explicit `# @tag` says otherwise) |
|
|
263
309
|
|
|
264
310
|
Useful options: `--select 'marts.*'` (repeatable), `--no-sources`, `--package all`, `-o schema.txt` for a single file.
|
|
265
311
|
|
|
@@ -313,7 +359,7 @@ jobs:
|
|
|
313
359
|
permissions: { contents: read, pull-requests: write }
|
|
314
360
|
steps:
|
|
315
361
|
- uses: actions/checkout@v4
|
|
316
|
-
- uses: MarkPhamm/quickdbd-cli@v0.1.
|
|
362
|
+
- uses: MarkPhamm/quickdbd-cli@v0.1.9
|
|
317
363
|
with:
|
|
318
364
|
path: erd/ # schema file(s) or folder(s)
|
|
319
365
|
output: site/index.html
|
|
@@ -336,7 +382,7 @@ Run your dbt setup first, then let the action generate the schema:
|
|
|
336
382
|
|
|
337
383
|
```yaml
|
|
338
384
|
# … dbt deps && dbt build && dbt docs generate
|
|
339
|
-
- uses: MarkPhamm/quickdbd-cli@v0.1.
|
|
385
|
+
- uses: MarkPhamm/quickdbd-cli@v0.1.9
|
|
340
386
|
with:
|
|
341
387
|
dbt-manifest: target/
|
|
342
388
|
dbt-args: --infer-fks
|
|
@@ -351,7 +397,7 @@ Commit `erd/` so pull requests can be compared with `main`. Set `dbt-check: true
|
|
|
351
397
|
```yaml
|
|
352
398
|
repos:
|
|
353
399
|
- repo: https://github.com/MarkPhamm/quickdbd-cli
|
|
354
|
-
rev: v0.1.
|
|
400
|
+
rev: v0.1.9
|
|
355
401
|
hooks:
|
|
356
402
|
- id: qdbd-check
|
|
357
403
|
files: ^erd/.*\.txt$
|
|
@@ -376,8 +422,9 @@ Builds are deterministic (no timestamps or randomness), so the output only chang
|
|
|
376
422
|
|
|
377
423
|
```bash
|
|
378
424
|
qdbd check PATH... [--strict] # validate
|
|
379
|
-
qdbd serve PATH... [--port 8765]
|
|
425
|
+
qdbd serve PATH... [--port 8765] [--read-only] # live editor + drag to arrange
|
|
380
426
|
qdbd build PATH... -o diagram.html # interactive HTML
|
|
427
|
+
qdbd share PATH... [--public] # publish as a gist, print a link
|
|
381
428
|
qdbd export PATH... -o out.png|.svg|.drawio [--focus T --depth 1]
|
|
382
429
|
qdbd layout PATH... [--reset] # (re)write the layout file
|
|
383
430
|
qdbd path PATH... --from A --to B # shortest join path + SQL JOIN
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "quickdbd-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
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"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.9"
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
var st = model.style, w = g.w, h = g.h, out = [];
|
|
79
79
|
out.push('<g class="t" data-id="' + esc(t.id) + '" transform="translate(' + n(p[0]) + "," + n(p[1]) + ')">');
|
|
80
80
|
out.push('<rect class="tb" width="' + w + '" height="' + h + '" rx="2" fill="' + C.body + '" stroke="' + C.border + '"/>');
|
|
81
|
-
var state = t.diff && t.diff.state, hcol = state === "added" ? C.added : state === "changed" ? C.changed : C.header;
|
|
81
|
+
var state = t.diff && t.diff.state, hcol = state === "added" ? C.added : state === "changed" ? C.changed : t.color || C.header;
|
|
82
82
|
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="' + hcol + '"/>');
|
|
83
83
|
if (state) {
|
|
84
84
|
out.push('<text x="' + (w - st.padX) + '" y="' + n(st.headerH / 2 + 4) + '" text-anchor="end" fill="#fff" font-family="' + FONT +
|
|
@@ -310,7 +310,7 @@
|
|
|
310
310
|
cells.push(cell({
|
|
311
311
|
id: tid, value: t.name, vertex: "1", parent: "1",
|
|
312
312
|
style: "shape=table;startSize=" + HEADH + ";container=1;collapsible=1;childLayout=tableLayout;fixedRows=1;rowLines=0;" +
|
|
313
|
-
"fontStyle=1;align=left;spacingLeft=10;resizeLast=1;html=0;whiteSpace=wrap;fillColor
|
|
313
|
+
"fontStyle=1;align=left;spacingLeft=10;resizeLast=1;html=0;whiteSpace=wrap;fillColor=" + (t.color || C.header) + ";fontColor=#ffffff;" +
|
|
314
314
|
"strokeColor=#dfe3e8;swimlaneFillColor=#ffffff;fontSize=14;",
|
|
315
315
|
}, g(p[0] - b[0] + 40, p[1] - b[1] + 40, w, HEADH + Math.max(tg.cols.length, 0) * ROWH)));
|
|
316
316
|
tg.cols.forEach(function (c, i) {
|
|
@@ -350,7 +350,7 @@
|
|
|
350
350
|
/* ------------------------------------------------------------------ selectors (dbt-style, over relationships)
|
|
351
351
|
* Space-separated atoms are unioned; a comma inside an atom intersects (a,b); a leading ! excludes.
|
|
352
352
|
* Atom: [N]+ method:pattern +[N]
|
|
353
|
-
* method: name (default; also matches aliases), group, file, col
|
|
353
|
+
* method: name (default; also matches aliases), group, file, col, tag
|
|
354
354
|
* pattern: glob with * and ?
|
|
355
355
|
* +x x plus the tables it references (parents), any depth; 2+x limits to 2 hops
|
|
356
356
|
* x+ x plus the tables referencing it (children); x+1 limits to 1 hop
|
|
@@ -391,11 +391,12 @@
|
|
|
391
391
|
else if (method === "group") ok = re.test(t.group || "") || (t.group || "").toLowerCase().indexOf(pat.toLowerCase() + "/") === 0;
|
|
392
392
|
else if (method === "file") ok = re.test(t.file || "") || re.test((t.file || "").split("/").pop());
|
|
393
393
|
else if (method === "col" || method === "column") ok = t.cols.some(function (c) { return re.test(c.name); });
|
|
394
|
+
else if (method === "tag") ok = (t.tags || []).some(function (x) { return re.test(x); });
|
|
394
395
|
else if (method === "diff" || method === "state") {
|
|
395
396
|
var ds = t.diff && t.diff.state, want = pat.toLowerCase();
|
|
396
397
|
ok = !!ds && (want === "modified" || want === "*" || want === ds || (want === "new" && ds === "added"));
|
|
397
398
|
}
|
|
398
|
-
else throw new Error("unknown selector method '" + method + "' (use name, group, file, col, diff)");
|
|
399
|
+
else throw new Error("unknown selector method '" + method + "' (use name, group, file, col, tag, diff)");
|
|
399
400
|
if (ok) { out[t.id] = true; base.push(t.id); }
|
|
400
401
|
});
|
|
401
402
|
function hops(op, before) {
|
|
@@ -56,6 +56,17 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
|
|
|
56
56
|
.fstat { font-size: 11px; color: var(--muted); padding: 3px 2px 0; min-height: 14px; }
|
|
57
57
|
.fstat.bad { color: #c0392b; }
|
|
58
58
|
.fstat a { color: var(--blue); cursor: pointer; margin-left: 6px; }
|
|
59
|
+
.tagbar { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 0 2px; }
|
|
60
|
+
.tchip { display: inline-flex; align-items: center; gap: 4px; font: 11px/1.6 inherit; padding: 0 7px 0 5px; border: 1px solid var(--line);
|
|
61
|
+
border-radius: 10px; background: #fff; color: var(--text, #333); cursor: pointer; }
|
|
62
|
+
.tchip i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
63
|
+
.tchip small { color: var(--muted); }
|
|
64
|
+
.tchip:hover { border-color: #b8c2d0; }
|
|
65
|
+
.tchip.on { border-color: var(--blue); background: #eef3fa; }
|
|
66
|
+
.tdot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin: 0 6px 1px -13px; vertical-align: middle; }
|
|
67
|
+
.tree.flat .tdot { margin-left: 0; }
|
|
68
|
+
.details .ttags { margin: -4px 0 10px; display: flex; gap: 4px; flex-wrap: wrap; }
|
|
69
|
+
.details .ttag { color: #fff; font-size: 11px; padding: 1px 8px; border-radius: 10px; cursor: pointer; text-decoration: none; }
|
|
59
70
|
.tree { overflow: auto; padding: 4px 0 8px; }
|
|
60
71
|
.explorer.collapsed .tree { display: none; }
|
|
61
72
|
.gh { padding: 6px 12px 4px; font-size: 11px; font-weight: bold; color: var(--muted); text-transform: uppercase;
|
|
@@ -331,7 +331,8 @@
|
|
|
331
331
|
var open = q || !shut[g];
|
|
332
332
|
var rows = items.map(function (t) {
|
|
333
333
|
if (open) visible.push(t.id);
|
|
334
|
-
return '<div class="ti" data-id="' + Q.esc(t.id) + '">' +
|
|
334
|
+
return '<div class="ti" data-id="' + Q.esc(t.id) + '">' +
|
|
335
|
+
(t.color ? '<i class="tdot" style="background:' + Q.esc(t.color) + '" title="' + Q.esc(t.tags.join(", ")) + '"></i>' : "") + Q.esc(t.name) +
|
|
335
336
|
(t.aliases.length ? "<small>as " + Q.esc(t.aliases.join(", ")) + "</small>" : "") + "</div>";
|
|
336
337
|
}).join("");
|
|
337
338
|
if (flat) return rows;
|
|
@@ -408,7 +409,25 @@
|
|
|
408
409
|
if (err) fstat.textContent = err;
|
|
409
410
|
else if (ids) fstat.innerHTML = "Showing " + ids.length + " of " + S.model.tables.length + ' tables<a id="fclear">clear</a>';
|
|
410
411
|
else fstat.textContent = "";
|
|
412
|
+
drawTags();
|
|
413
|
+
}
|
|
414
|
+
/* Tag legend: one chip per tag (header colour + count); a click filters to tag:NAME, a second click clears. */
|
|
415
|
+
var tagbar = document.getElementById("tagbar");
|
|
416
|
+
function drawTags() {
|
|
417
|
+
var tags = S.model.tags || [];
|
|
418
|
+
tagbar.style.display = tags.length ? "" : "none";
|
|
419
|
+
tagbar.innerHTML = tags.map(function (g) {
|
|
420
|
+
var on = S.filter.toLowerCase() === "tag:" + g.name;
|
|
421
|
+
return '<button class="tchip' + (on ? " on" : "") + '" data-tag="' + Q.esc(g.name) + '" title="Show only tag:' + Q.esc(g.name) +
|
|
422
|
+
'"><i style="background:' + Q.esc(g.color) + '"></i>' + Q.esc(g.name) + "<small>" + g.count + "</small></button>";
|
|
423
|
+
}).join("");
|
|
411
424
|
}
|
|
425
|
+
tagbar.addEventListener("click", function (ev) {
|
|
426
|
+
var b = ev.target.closest(".tchip");
|
|
427
|
+
if (!b) return;
|
|
428
|
+
var expr = "tag:" + b.getAttribute("data-tag");
|
|
429
|
+
applyFilter(S.filter.toLowerCase() === expr ? "" : expr);
|
|
430
|
+
});
|
|
412
431
|
function applyFilter(expr, keepView) {
|
|
413
432
|
S.filter = (expr || "").trim();
|
|
414
433
|
if (finput.value.trim() !== S.filter) finput.value = S.filter;
|
|
@@ -476,6 +495,12 @@
|
|
|
476
495
|
if (t.aliases.length) meta.push("as " + Q.esc(t.aliases.join(", ")));
|
|
477
496
|
if (t.file) meta.push(Q.esc(t.file) + ":" + t.line);
|
|
478
497
|
if (meta.length) out.push('<div class="meta">' + meta.join(" · ") + "</div>");
|
|
498
|
+
if (t.tags) {
|
|
499
|
+
out.push('<div class="ttags">' + t.tags.map(function (g) {
|
|
500
|
+
var c = (S.model.tags || []).filter(function (x) { return x.name === g; })[0];
|
|
501
|
+
return '<a class="ttag" data-tag="' + Q.esc(g) + '" style="background:' + Q.esc(c ? c.color : "#2b5797") + '">' + Q.esc(g) + "</a>";
|
|
502
|
+
}).join("") + "</div>");
|
|
503
|
+
}
|
|
479
504
|
if (t.doc) out.push('<div class="doc">' + Q.esc(t.doc) + "</div>");
|
|
480
505
|
out.push('<div class="pathbox"><input id="pathto" list="pathlist" placeholder="Path to another table…" autocomplete="off" value="' +
|
|
481
506
|
(S.path && byId[S.path.to] ? Q.esc(byId[S.path.to].name) : "") + '"></div><datalist id="pathlist">' +
|
|
@@ -558,6 +583,8 @@
|
|
|
558
583
|
dbody.addEventListener("click", function (ev) {
|
|
559
584
|
var a = ev.target.closest("a[data-go]");
|
|
560
585
|
if (a) select(a.getAttribute("data-go"), { center: true });
|
|
586
|
+
var tg = ev.target.closest("a.ttag");
|
|
587
|
+
if (tg) applyFilter("tag:" + tg.getAttribute("data-tag"));
|
|
561
588
|
});
|
|
562
589
|
document.getElementById("dclose").onclick = function () { select(null); };
|
|
563
590
|
|
|
@@ -570,7 +597,7 @@
|
|
|
570
597
|
var geo = Q.geometry(S.vm, S.mode), parts = [];
|
|
571
598
|
S.vm.tables.forEach(function (t) {
|
|
572
599
|
var p = S.pos[t.id];
|
|
573
|
-
if (p) parts.push('<rect x="' + p[0] + '" y="' + p[1] + '" width="' + geo[t.id].w + '" height="' + geo[t.id].h + '" fill="#2b5797" opacity=".55"/>');
|
|
600
|
+
if (p) parts.push('<rect x="' + p[0] + '" y="' + p[1] + '" width="' + geo[t.id].w + '" height="' + geo[t.id].h + '" fill="' + (t.color || "#2b5797") + '" opacity=".55"/>');
|
|
574
601
|
});
|
|
575
602
|
parts.push('<rect class="vpr" id="vpr"/>');
|
|
576
603
|
mini.innerHTML = parts.join("");
|
|
@@ -47,7 +47,8 @@ def cmd_check(args) -> int:
|
|
|
47
47
|
return 0 if ok else 1
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
def
|
|
50
|
+
def _built_page(args) -> tuple[str, dict] | int:
|
|
51
|
+
"""Compile, select and lay out like `build`; returns (html, model) or an exit code."""
|
|
51
52
|
from .html import render_page
|
|
52
53
|
|
|
53
54
|
project = _open(args)
|
|
@@ -80,13 +81,41 @@ def cmd_build(args) -> int:
|
|
|
80
81
|
print(f"error: {e}", file=sys.stderr)
|
|
81
82
|
return 2
|
|
82
83
|
raise
|
|
84
|
+
return render_page(model, pos, _title(project)), model
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_build(args) -> int:
|
|
88
|
+
res = _built_page(args)
|
|
89
|
+
if isinstance(res, int):
|
|
90
|
+
return res
|
|
91
|
+
page, model = res
|
|
83
92
|
out = Path(args.output)
|
|
84
93
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
-
out.write_text(
|
|
94
|
+
out.write_text(page, encoding="utf-8")
|
|
86
95
|
print(f"Wrote {out} ({len(model['tables'])} tables, {len(model['rels'])} relationships)")
|
|
87
96
|
return 0
|
|
88
97
|
|
|
89
98
|
|
|
99
|
+
def cmd_share(args) -> int:
|
|
100
|
+
from . import share
|
|
101
|
+
|
|
102
|
+
res = _built_page(args)
|
|
103
|
+
if isinstance(res, int):
|
|
104
|
+
return res
|
|
105
|
+
page, model = res
|
|
106
|
+
key = share.share_key(args.paths, selector_expr(args.select, args.exclude))
|
|
107
|
+
try:
|
|
108
|
+
r = share.publish(page, _title(_open(args)), key, public=args.public, new=args.new)
|
|
109
|
+
except share.ShareError as e:
|
|
110
|
+
print(f"error: {e}", file=sys.stderr)
|
|
111
|
+
return 2
|
|
112
|
+
verb = "Updated" if r.updated else "Shared"
|
|
113
|
+
print(f"{verb} {len(model['tables'])} tables ({'public' if r.public else 'secret'} gist {r.gist_url})")
|
|
114
|
+
print(f"Link: {r.link}")
|
|
115
|
+
print(f" (if that doesn't load: {r.alt_link})")
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
90
119
|
def cmd_export(args) -> int:
|
|
91
120
|
from . import export
|
|
92
121
|
|
|
@@ -289,7 +318,7 @@ def cmd_serve(args) -> int:
|
|
|
289
318
|
from .serve import serve
|
|
290
319
|
|
|
291
320
|
project = _open(args)
|
|
292
|
-
return serve(project, port=args.port, open_browser=not args.no_open)
|
|
321
|
+
return serve(project, port=args.port, open_browser=not args.no_open, read_only=args.read_only)
|
|
293
322
|
|
|
294
323
|
|
|
295
324
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -328,6 +357,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
328
357
|
sp.add_argument("--strict", action="store_true")
|
|
329
358
|
sp.set_defaults(func=cmd_build)
|
|
330
359
|
|
|
360
|
+
sp = sub.add_parser("share", help="publish the diagram as a GitHub gist and print a link (needs the gh CLI)")
|
|
361
|
+
paths(sp)
|
|
362
|
+
selectors(sp)
|
|
363
|
+
sp.add_argument(
|
|
364
|
+
"--diff-base", metavar="GIT_REF", help="colour tables/columns changed since this git ref (e.g. origin/main)"
|
|
365
|
+
)
|
|
366
|
+
sp.add_argument("--by-group", action="store_true", help=BY_GROUP_HELP)
|
|
367
|
+
sp.add_argument("--layout", help="layout sidecar file (default: next to the schema)")
|
|
368
|
+
sp.add_argument("--strict", action="store_true")
|
|
369
|
+
sp.add_argument("--public", action="store_true", help="list the gist publicly (default: secret, link-only)")
|
|
370
|
+
sp.add_argument("--new", action="store_true", help="make a new gist instead of updating the last one")
|
|
371
|
+
sp.set_defaults(func=cmd_share)
|
|
372
|
+
|
|
331
373
|
sp = sub.add_parser("export", help="write a PNG, SVG or draw.io file")
|
|
332
374
|
paths(sp)
|
|
333
375
|
sp.add_argument("-o", "--output", required=True, help="out.png, out.svg or out.drawio")
|
|
@@ -398,6 +440,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
398
440
|
sp.add_argument("--port", type=int, default=8765)
|
|
399
441
|
sp.add_argument("--layout")
|
|
400
442
|
sp.add_argument("--no-open", action="store_true", help="don't open a browser")
|
|
443
|
+
sp.add_argument(
|
|
444
|
+
"--read-only",
|
|
445
|
+
action="store_true",
|
|
446
|
+
help="live view only: no editor, nothing saved; safe to share through a tunnel",
|
|
447
|
+
)
|
|
401
448
|
sp.set_defaults(func=cmd_serve)
|
|
402
449
|
return p
|
|
403
450
|
|
|
@@ -113,6 +113,7 @@ class Tbl:
|
|
|
113
113
|
group: str
|
|
114
114
|
description: str = ""
|
|
115
115
|
cols: dict[str, Col] = field(default_factory=dict)
|
|
116
|
+
tags: list[str] = field(default_factory=list)
|
|
116
117
|
|
|
117
118
|
def col(self, name: str) -> Col:
|
|
118
119
|
key = name.lower()
|
|
@@ -173,6 +174,17 @@ def _comment_lines(text: str) -> list[str]:
|
|
|
173
174
|
return out
|
|
174
175
|
|
|
175
176
|
|
|
177
|
+
def _node_tags(n: dict) -> list[str]:
|
|
178
|
+
"""dbt tags (node-level and config), lowercased, in order, usable as qdbd tag names."""
|
|
179
|
+
out: list[str] = []
|
|
180
|
+
raw = list(n.get("tags") or []) + list((n.get("config") or {}).get("tags") or [])
|
|
181
|
+
for tag in raw:
|
|
182
|
+
tag = re.sub(r"[^a-z0-9_.-]+", "-", str(tag).lower()).strip("-.")
|
|
183
|
+
if tag and tag not in out:
|
|
184
|
+
out.append(tag)
|
|
185
|
+
return out
|
|
186
|
+
|
|
187
|
+
|
|
176
188
|
class _Importer:
|
|
177
189
|
def __init__(
|
|
178
190
|
self,
|
|
@@ -231,7 +243,7 @@ class _Importer:
|
|
|
231
243
|
name = self._base_name(n)
|
|
232
244
|
if names[name] > 1: # same name in two packages
|
|
233
245
|
name = f"{n.get('package_name')}.{name}"
|
|
234
|
-
t = Tbl(n["unique_id"], name, self._group(n), n.get("description") or "")
|
|
246
|
+
t = Tbl(n["unique_id"], name, self._group(n), n.get("description") or "", tags=_node_tags(n))
|
|
235
247
|
self._columns(t, n)
|
|
236
248
|
self.tables[t.uid] = t
|
|
237
249
|
self.by_name.setdefault(n.get("name", "").lower(), []).append(t.uid)
|
|
@@ -435,6 +447,8 @@ class _Importer:
|
|
|
435
447
|
# -- output -------------------------------------------------------------
|
|
436
448
|
def render_table(self, t: Tbl) -> list[str]:
|
|
437
449
|
lines = _comment_lines(t.description)
|
|
450
|
+
if t.tags:
|
|
451
|
+
lines.append(f"# @dbt-tags: {', '.join(t.tags)}")
|
|
438
452
|
lines += [quote(t.name), "-"]
|
|
439
453
|
for c in sorted(t.cols.values(), key=lambda c: (c.order, c.name.lower())):
|
|
440
454
|
lines += _comment_lines(c.description)
|
|
@@ -37,6 +37,7 @@ BODY = """
|
|
|
37
37
|
<input id="filter" type="text" placeholder="Filter: orders+1 group:marts !stg_*" autocomplete="off"
|
|
38
38
|
spellcheck="false" title="{filter_help}">
|
|
39
39
|
<div class="fstat" id="fstat"></div>
|
|
40
|
+
<div class="tagbar" id="tagbar"></div>
|
|
40
41
|
</div>
|
|
41
42
|
<div id="tree" class="tree"></div>
|
|
42
43
|
</div>
|
|
@@ -87,17 +88,31 @@ SIDE = """
|
|
|
87
88
|
|
|
88
89
|
FILTER_HELP = (
|
|
89
90
|
"dbt-style selectors. +x: x and what it references. x+: x and what references it. "
|
|
90
|
-
"N+x+N: hops. group:/file:/col: methods. a,b: both. !x: exclude."
|
|
91
|
+
"N+x+N: hops. group:/file:/col:/tag: methods. a,b: both. !x: exclude."
|
|
91
92
|
)
|
|
92
93
|
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
# Read-only live view: refetch the model whenever the server's version changes. Relative URLs, so it
|
|
96
|
+
# also works behind a tunnel or a path prefix.
|
|
97
|
+
LIVE_JS = (
|
|
98
|
+
"(function(){var v=null;new EventSource('events').onmessage=function(e){"
|
|
99
|
+
"if(v!==null&&e.data!==v)fetch('state').then(function(r){return r.json();})"
|
|
100
|
+
".then(function(s){window.qdbdViewer.setData(s.model,s.positions);});v=e.data;};})();"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def render_page(model: dict, positions: dict, title: str, serve: bool = False, live: bool = False) -> str:
|
|
105
|
+
"""The self-contained page. ``serve``: editor pane and saved dragging. ``live``: read-only, but reloads
|
|
106
|
+
when the server's schema changes (``qdbd serve --read-only``)."""
|
|
95
107
|
data = {"model": model, "positions": positions, "serve": serve, "title": title}
|
|
96
|
-
|
|
97
|
-
"Drag tables to arrange them (saved). Click a table for details. Esc clears."
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
if serve:
|
|
109
|
+
hint = "Drag tables to arrange them (saved). Click a table for details. Esc clears."
|
|
110
|
+
elif live:
|
|
111
|
+
hint = "Live, read-only view: updates when the schema changes. Drag tables to rearrange (not saved)."
|
|
112
|
+
else:
|
|
113
|
+
hint = (
|
|
114
|
+
"Drag tables to rearrange (not saved). Click a table for details. Scroll to zoom, drag empty space to pan."
|
|
115
|
+
)
|
|
101
116
|
body = BODY.format(side=SIDE if serve else "", count=len(model["tables"]), hint=hint, filter_help=FILTER_HELP)
|
|
102
117
|
head_extra = ""
|
|
103
118
|
scripts = ""
|
|
@@ -108,6 +123,8 @@ def render_page(model: dict, positions: dict, title: str, serve: bool = False) -
|
|
|
108
123
|
f'<script src="{CODEMIRROR}/addon/mode/simple.min.js"></script>'
|
|
109
124
|
f"<script>{_asset('editor.js')}</script>"
|
|
110
125
|
)
|
|
126
|
+
elif live:
|
|
127
|
+
scripts = f"<script>{LIVE_JS}</script>"
|
|
111
128
|
return (
|
|
112
129
|
'<!doctype html>\n<html lang="en"><head><meta charset="utf-8">'
|
|
113
130
|
'<meta name="viewport" content="width=device-width, initial-scale=1">'
|
|
@@ -11,6 +11,7 @@ from functools import cache
|
|
|
11
11
|
from importlib import resources
|
|
12
12
|
from pathlib import PurePosixPath
|
|
13
13
|
|
|
14
|
+
from . import tags
|
|
14
15
|
from .model import Column, Schema, Table
|
|
15
16
|
|
|
16
17
|
STYLE = {
|
|
@@ -127,12 +128,17 @@ def model_json(schema: Schema) -> dict:
|
|
|
127
128
|
"b": {"t": r.b.table, "c": r.b.column, "card": r.b.card},
|
|
128
129
|
}
|
|
129
130
|
)
|
|
130
|
-
|
|
131
|
+
tables = [
|
|
132
|
+
{**table_json(t), "group": groups[t.path], "file": t.path, "line": t.line} for t in schema.tables.values()
|
|
133
|
+
]
|
|
134
|
+
legend = tags.annotate(schema, tables)
|
|
135
|
+
out = {
|
|
131
136
|
"style": STYLE,
|
|
132
|
-
"tables":
|
|
133
|
-
{**table_json(t), "group": groups[t.path], "file": t.path, "line": t.line} for t in schema.tables.values()
|
|
134
|
-
],
|
|
137
|
+
"tables": tables,
|
|
135
138
|
"rels": rels,
|
|
136
139
|
"zoom": schema.zoom,
|
|
137
140
|
"position": list(schema.position) if schema.position else None,
|
|
138
141
|
}
|
|
142
|
+
if legend:
|
|
143
|
+
out["tags"] = legend
|
|
144
|
+
return out
|
|
@@ -58,6 +58,8 @@ class Table:
|
|
|
58
58
|
aliases: list[str] = field(default_factory=list)
|
|
59
59
|
columns: dict[str, Column] = field(default_factory=dict)
|
|
60
60
|
comments: list[str] = field(default_factory=list)
|
|
61
|
+
tags: list[str] = field(default_factory=list) # `# @tag: ...` comments
|
|
62
|
+
dbt_tags: list[str] = field(default_factory=list) # `# @dbt-tags: ...`, written by `qdbd import dbt`
|
|
61
63
|
|
|
62
64
|
|
|
63
65
|
@dataclass
|
|
@@ -84,6 +86,7 @@ class Schema:
|
|
|
84
86
|
layout: dict[str, tuple[float, float]] = field(default_factory=dict)
|
|
85
87
|
diagnostics: list[Diagnostic] = field(default_factory=list)
|
|
86
88
|
general_comments: list[str] = field(default_factory=list)
|
|
89
|
+
tag_colors: dict[str, str] = field(default_factory=dict) # `# @color TAG #hex`
|
|
87
90
|
|
|
88
91
|
@property
|
|
89
92
|
def errors(self) -> list[Diagnostic]:
|