quickdbd-cli 0.1.1__tar.gz → 0.1.2__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 (44) hide show
  1. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/PKG-INFO +3 -2
  2. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/README.md +2 -1
  3. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/pyproject.toml +1 -1
  4. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/scripts/release.sh +7 -2
  5. quickdbd_cli-0.1.2/src/quickdbd_cli/__init__.py +1 -0
  6. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/render.js +15 -4
  7. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/style.css +9 -0
  8. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/viewer.js +45 -2
  9. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/html.py +1 -0
  10. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/measure.py +10 -3
  11. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/tests/test_pipeline.py +31 -0
  12. quickdbd_cli-0.1.1/src/quickdbd_cli/__init__.py +0 -1
  13. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/.gitignore +0 -0
  14. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/LICENSE +0 -0
  15. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/NOTICE +0 -0
  16. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/examples/quickdbd-sample.txt +0 -0
  17. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/examples/synthetic/finance.txt +0 -0
  18. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/examples/synthetic/hr.txt +0 -0
  19. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/examples/synthetic/inventory.txt +0 -0
  20. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/examples/synthetic/marketing.txt +0 -0
  21. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/examples/synthetic/qdbd.layout.json +0 -0
  22. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/examples/synthetic/sales.txt +0 -0
  23. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/scripts/gen_font_metrics.py +0 -0
  24. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/scripts/gen_schema.py +0 -0
  25. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/__main__.py +0 -0
  26. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  27. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  28. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  29. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  30. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  31. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/editor.js +0 -0
  32. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  33. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  34. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  35. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/cli.py +0 -0
  36. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/export.py +0 -0
  37. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/jsrt.py +0 -0
  38. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/layout.py +0 -0
  39. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/model.py +0 -0
  40. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/parser.py +0 -0
  41. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/project.py +0 -0
  42. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/src/quickdbd_cli/serve.py +0 -0
  43. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/tests/test_parser.py +0 -0
  44. {quickdbd_cli-0.1.1 → quickdbd_cli-0.1.2}/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.2
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,6 +55,7 @@ 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.
@@ -157,7 +158,7 @@ TWINE_USERNAME=__token__
157
158
  TWINE_PASSWORD=pypi-...
158
159
  ```
159
160
 
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.
161
+ 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
162
 
162
163
  ## License
163
164
 
@@ -30,6 +30,7 @@ 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.
@@ -132,7 +133,7 @@ TWINE_USERNAME=__token__
132
133
  TWINE_PASSWORD=pypi-...
133
134
  ```
134
135
 
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.
136
+ 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
137
 
137
138
  ## License
138
139
 
@@ -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.2"
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.2"
@@ -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;
@@ -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;
@@ -50,6 +50,7 @@ BODY = """
50
50
  <div class="minimap">
51
51
  <svg id="mini" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"></svg>
52
52
  </div>
53
+ <div class="tip" id="tip" role="tooltip"></div>
53
54
  <div class="hint">{hint}</div>
54
55
  </div>
55
56
  </div>
@@ -60,7 +60,7 @@ def _column_json(col: Column) -> dict:
60
60
  name_w = text_width(col.name, s["rowSize"], bold)
61
61
  bs = [{"t": b, "w": round(text_width(b, s["badgeSize"]) + 2 * s["badgePad"], 2)} for b in badges(col)]
62
62
  type_w = text_width(col.type or "", s["rowSize"])
63
- return {
63
+ out = {
64
64
  "id": col.id,
65
65
  "name": col.name,
66
66
  "type": col.type or "",
@@ -75,12 +75,16 @@ def _column_json(col: Column) -> dict:
75
75
  "typeW": type_w,
76
76
  "badges": bs,
77
77
  }
78
+ if col.comments:
79
+ out["doc"] = "\n".join(col.comments)
80
+ return out
78
81
 
79
82
 
80
83
  def table_json(table: Table) -> dict:
81
84
  s = STYLE
82
85
  cols = [_column_json(c) for c in table.columns.values() if not c.virtual]
83
- w = text_width(table.name, s["headerSize"]) + 2 * s["padX"]
86
+ name_w = text_width(table.name, s["headerSize"])
87
+ w = name_w + 2 * s["padX"]
84
88
  for c in cols:
85
89
  row = c["nameW"] + c["typeW"] + 2 * s["padX"] + s["gap"]
86
90
  row += sum(b["w"] + s["badgeGap"] for b in c["badges"])
@@ -89,7 +93,10 @@ def table_json(table: Table) -> dict:
89
93
  w = max(w, row)
90
94
  w = max(round(w), s["minW"])
91
95
  h = s["headerH"] + len(cols) * s["rowH"] + s["bottomPad"]
92
- return {"id": table.id, "name": table.name, "aliases": table.aliases, "w": w, "h": h, "cols": cols}
96
+ out = {"id": table.id, "name": table.name, "aliases": table.aliases, "w": w, "h": h, "nameW": name_w, "cols": cols}
97
+ if table.comments:
98
+ out["doc"] = "\n".join(table.comments)
99
+ return out
93
100
 
94
101
 
95
102
  def model_json(schema: Schema) -> dict:
@@ -186,3 +186,34 @@ def test_missing_path(capsys):
186
186
  @pytest.mark.parametrize("bad_suffix", ["out.jpg"])
187
187
  def test_export_rejects_unknown_format(tmp_path, bad_suffix):
188
188
  assert main(["export", str(SAMPLE), "-o", str(tmp_path / bad_suffix)]) == 2
189
+
190
+
191
+ DOCS = """# Customers who placed at least one order
192
+ Customer # one row per person
193
+ -
194
+ CustomerId PK int
195
+ # Contact address
196
+ Email string # verified
197
+ Name string
198
+ """
199
+
200
+
201
+ def test_doc_comments_in_model():
202
+ m = model_json(compile_text(DOCS))
203
+ t = m["tables"][0]
204
+ assert t["doc"] == "Customers who placed at least one order\none row per person"
205
+ cols = {c["id"]: c for c in t["cols"]}
206
+ assert cols["Email"]["doc"] == "Contact address\nverified"
207
+ assert "doc" not in cols["Name"]
208
+
209
+
210
+ def test_doc_marks_only_in_viewer_not_exports():
211
+ from quickdbd_cli.jsrt import runtime
212
+
213
+ m = model_json(compile_text(DOCS))
214
+ pos = {"Customer": [0, 0]}
215
+ rt = runtime()
216
+ rt.load("render.js")
217
+ viewer = rt.call("qdbd.renderInner", m, pos, "full", {"docMarks": True})
218
+ assert viewer.count('class="dm"') == 2 # table header + Email
219
+ assert 'class="dm"' not in export.render_svg(m, pos, embed_font=False)
@@ -1 +0,0 @@
1
- __version__ = "0.1.1"
File without changes
File without changes
File without changes