quickdbd-cli 0.1.9__tar.gz → 0.1.10__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 (58) hide show
  1. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/PKG-INFO +4 -4
  2. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/README.md +3 -3
  3. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/pyproject.toml +1 -1
  4. quickdbd_cli-0.1.10/src/quickdbd_cli/__init__.py +1 -0
  5. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/cli.py +4 -2
  6. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/serve.py +30 -5
  7. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_serve.py +33 -0
  8. quickdbd_cli-0.1.9/src/quickdbd_cli/__init__.py +0 -1
  9. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/.gitignore +0 -0
  10. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/LICENSE +0 -0
  11. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/NOTICE +0 -0
  12. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/examples/quickdbd-sample.txt +0 -0
  13. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/examples/synthetic/finance.txt +0 -0
  14. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/examples/synthetic/hr.txt +0 -0
  15. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/examples/synthetic/inventory.txt +0 -0
  16. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/examples/synthetic/marketing.txt +0 -0
  17. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/examples/synthetic/qdbd.layout.json +0 -0
  18. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/examples/synthetic/sales.txt +0 -0
  19. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/scripts/gen_font_metrics.py +0 -0
  20. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/scripts/gen_schema.py +0 -0
  21. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/scripts/release.sh +0 -0
  22. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/__main__.py +0 -0
  23. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  24. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  25. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  26. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  27. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  28. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/editor.js +0 -0
  29. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  30. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  31. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  32. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/render.js +0 -0
  33. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/style.css +0 -0
  34. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/assets/viewer.js +0 -0
  35. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/dbt.py +0 -0
  36. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/diff.py +0 -0
  37. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/export.py +0 -0
  38. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/html.py +0 -0
  39. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/jsrt.py +0 -0
  40. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/layout.py +0 -0
  41. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/measure.py +0 -0
  42. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/model.py +0 -0
  43. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/parser.py +0 -0
  44. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/project.py +0 -0
  45. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/share.py +0 -0
  46. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/src/quickdbd_cli/tags.py +0 -0
  47. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/fixtures/jaffle/LICENSE +0 -0
  48. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/fixtures/jaffle/README.md +0 -0
  49. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/fixtures/jaffle/catalog.json +0 -0
  50. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/fixtures/jaffle/manifest.json +0 -0
  51. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_dbt.py +0 -0
  52. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_diff.py +0 -0
  53. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_parser.py +0 -0
  54. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_path.py +0 -0
  55. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_pipeline.py +0 -0
  56. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_select.py +0 -0
  57. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_share.py +0 -0
  58. {quickdbd_cli-0.1.9 → quickdbd_cli-0.1.10}/tests/test_tags.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.1.9
3
+ Version: 0.1.10
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
@@ -387,7 +387,7 @@ jobs:
387
387
  permissions: { contents: read, pull-requests: write }
388
388
  steps:
389
389
  - uses: actions/checkout@v4
390
- - uses: MarkPhamm/quickdbd-cli@v0.1.9
390
+ - uses: MarkPhamm/quickdbd-cli@v0.1.10
391
391
  with:
392
392
  path: erd/ # schema file(s) or folder(s)
393
393
  output: site/index.html
@@ -410,7 +410,7 @@ Run your dbt setup first, then let the action generate the schema:
410
410
 
411
411
  ```yaml
412
412
  # … dbt deps && dbt build && dbt docs generate
413
- - uses: MarkPhamm/quickdbd-cli@v0.1.9
413
+ - uses: MarkPhamm/quickdbd-cli@v0.1.10
414
414
  with:
415
415
  dbt-manifest: target/
416
416
  dbt-args: --infer-fks
@@ -425,7 +425,7 @@ Commit `erd/` so pull requests can be compared with `main`. Set `dbt-check: true
425
425
  ```yaml
426
426
  repos:
427
427
  - repo: https://github.com/MarkPhamm/quickdbd-cli
428
- rev: v0.1.9
428
+ rev: v0.1.10
429
429
  hooks:
430
430
  - id: qdbd-check
431
431
  files: ^erd/.*\.txt$
@@ -359,7 +359,7 @@ jobs:
359
359
  permissions: { contents: read, pull-requests: write }
360
360
  steps:
361
361
  - uses: actions/checkout@v4
362
- - uses: MarkPhamm/quickdbd-cli@v0.1.9
362
+ - uses: MarkPhamm/quickdbd-cli@v0.1.10
363
363
  with:
364
364
  path: erd/ # schema file(s) or folder(s)
365
365
  output: site/index.html
@@ -382,7 +382,7 @@ Run your dbt setup first, then let the action generate the schema:
382
382
 
383
383
  ```yaml
384
384
  # … dbt deps && dbt build && dbt docs generate
385
- - uses: MarkPhamm/quickdbd-cli@v0.1.9
385
+ - uses: MarkPhamm/quickdbd-cli@v0.1.10
386
386
  with:
387
387
  dbt-manifest: target/
388
388
  dbt-args: --infer-fks
@@ -397,7 +397,7 @@ Commit `erd/` so pull requests can be compared with `main`. Set `dbt-check: true
397
397
  ```yaml
398
398
  repos:
399
399
  - repo: https://github.com/MarkPhamm/quickdbd-cli
400
- rev: v0.1.9
400
+ rev: v0.1.10
401
401
  hooks:
402
402
  - id: qdbd-check
403
403
  files: ^erd/.*\.txt$
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quickdbd-cli"
7
- version = "0.1.9"
7
+ version = "0.1.10"
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.10"
@@ -318,7 +318,9 @@ def cmd_serve(args) -> int:
318
318
  from .serve import serve
319
319
 
320
320
  project = _open(args)
321
- return serve(project, port=args.port, open_browser=not args.no_open, read_only=args.read_only)
321
+ # Without --port, a busy 8765 (e.g. a second `qdbd serve`) moves on to the next free port.
322
+ port, tries = (args.port, 1) if args.port is not None else (8765, 20)
323
+ return serve(project, port=port, open_browser=not args.no_open, read_only=args.read_only, tries=tries)
322
324
 
323
325
 
324
326
  def build_parser() -> argparse.ArgumentParser:
@@ -437,7 +439,7 @@ def build_parser() -> argparse.ArgumentParser:
437
439
 
438
440
  sp = sub.add_parser("serve", help="live editor: reload on save, drag tables to arrange")
439
441
  paths(sp)
440
- sp.add_argument("--port", type=int, default=8765)
442
+ sp.add_argument("--port", type=int, help="port to listen on (default: 8765, or the next free one)")
441
443
  sp.add_argument("--layout")
442
444
  sp.add_argument("--no-open", action="store_true", help="don't open a browser")
443
445
  sp.add_argument(
@@ -9,6 +9,7 @@ the server never writes anything.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import errno
12
13
  import json
13
14
  import sys
14
15
  import threading
@@ -190,11 +191,29 @@ def _handler(state: State, title: str):
190
191
  return Handler
191
192
 
192
193
 
193
- def make_server(project: Project, port: int = 8765, read_only: bool = False) -> tuple[ThreadingHTTPServer, State]:
194
- """Bind the HTTP server (port 0 picks a free port) without starting it."""
194
+ class PortInUse(OSError):
195
+ pass
196
+
197
+
198
+ def make_server(
199
+ project: Project, port: int = 8765, read_only: bool = False, tries: int = 1
200
+ ) -> tuple[ThreadingHTTPServer, State]:
201
+ """Bind the HTTP server without starting it. Port 0 picks a free port; with ``tries`` > 1, a busy port
202
+ moves on to the next one (port, port+1, ...)."""
195
203
  state = State(project, read_only=read_only)
196
204
  title = project.inputs[0].stem if len(project.inputs) == 1 else "diagram"
197
- server = ThreadingHTTPServer(("127.0.0.1", port), _handler(state, title))
205
+ server = None
206
+ for p in range(port, port + max(tries, 1)) if port else [0]:
207
+ try:
208
+ server = ThreadingHTTPServer(("127.0.0.1", p), _handler(state, title))
209
+ break
210
+ except OSError as e:
211
+ if e.errno != errno.EADDRINUSE:
212
+ raise
213
+ if server is None:
214
+ last = port + max(tries, 1) - 1
215
+ span = f"ports {port}-{last} are" if last > port else f"port {port} is"
216
+ raise PortInUse(f"{span} already in use (another qdbd serve?); pick one with --port")
198
217
  server.daemon_threads = True
199
218
  return server, state
200
219
 
@@ -210,8 +229,14 @@ def poll_once(state: State) -> bool:
210
229
  return True
211
230
 
212
231
 
213
- def serve(project: Project, port: int = 8765, open_browser: bool = True, read_only: bool = False) -> int:
214
- server, state = make_server(project, port, read_only=read_only)
232
+ def serve(
233
+ project: Project, port: int = 8765, open_browser: bool = True, read_only: bool = False, tries: int = 1
234
+ ) -> int:
235
+ try:
236
+ server, state = make_server(project, port, read_only=read_only, tries=tries)
237
+ except PortInUse as e:
238
+ print(f"error: {e}", file=sys.stderr)
239
+ return 2
215
240
  for d in state.diagnostics:
216
241
  print(d["text"], file=sys.stderr)
217
242
  url = f"http://127.0.0.1:{server.server_address[1]}/"
@@ -194,3 +194,36 @@ def test_read_only_refuses_writes_and_never_touches_disk(ro):
194
194
  assert poll_once(state)
195
195
  assert [t["name"] for t in call(base, "/state")[1]["model"]["tables"]] == ["Customer", "Order", "Product"]
196
196
  assert not f.with_name("shop.layout.json").exists()
197
+
198
+
199
+ # ------------------------------------------------------------------ busy ports
200
+
201
+
202
+ def test_busy_port_moves_to_the_next_free_one(tmp_path):
203
+ from quickdbd_cli.serve import PortInUse
204
+
205
+ f = tmp_path / "shop.txt"
206
+ f.write_text(SCHEMA)
207
+ first, _ = make_server(Project.open([f]), port=0)
208
+ busy = first.server_address[1]
209
+ try:
210
+ with pytest.raises(PortInUse, match=f"port {busy} is already in use"):
211
+ make_server(Project.open([f]), port=busy)
212
+ second, _ = make_server(Project.open([f]), port=busy, tries=20)
213
+ assert second.server_address[1] > busy
214
+ second.server_close()
215
+ finally:
216
+ first.server_close()
217
+
218
+
219
+ def test_cli_explicit_busy_port_is_a_clean_error(tmp_path, capsys):
220
+ from quickdbd_cli.cli import main
221
+
222
+ f = tmp_path / "shop.txt"
223
+ f.write_text(SCHEMA)
224
+ first, _ = make_server(Project.open([f]), port=0)
225
+ try:
226
+ assert main(["serve", str(f), "--no-open", "--port", str(first.server_address[1])]) == 2
227
+ assert "already in use" in capsys.readouterr().err
228
+ finally:
229
+ first.server_close()
@@ -1 +0,0 @@
1
- __version__ = "0.1.9"
File without changes
File without changes
File without changes