quickdbd-cli 0.1.0__tar.gz → 0.1.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 (44) hide show
  1. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/PKG-INFO +12 -1
  2. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/README.md +11 -0
  3. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/examples/synthetic/qdbd.layout.json +14 -14
  4. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/pyproject.toml +1 -1
  5. quickdbd_cli-0.1.1/scripts/release.sh +23 -0
  6. quickdbd_cli-0.1.1/src/quickdbd_cli/__init__.py +1 -0
  7. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/viewer.js +1 -1
  8. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/html.py +5 -2
  9. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/serve.py +21 -8
  10. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/tests/test_pipeline.py +20 -0
  11. quickdbd_cli-0.1.1/tests/test_serve.py +160 -0
  12. quickdbd_cli-0.1.0/src/quickdbd_cli/__init__.py +0 -1
  13. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/.gitignore +0 -0
  14. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/LICENSE +0 -0
  15. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/NOTICE +0 -0
  16. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/examples/quickdbd-sample.txt +0 -0
  17. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/examples/synthetic/finance.txt +0 -0
  18. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/examples/synthetic/hr.txt +0 -0
  19. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/examples/synthetic/inventory.txt +0 -0
  20. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/examples/synthetic/marketing.txt +0 -0
  21. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/examples/synthetic/sales.txt +0 -0
  22. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/scripts/gen_font_metrics.py +0 -0
  23. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/scripts/gen_schema.py +0 -0
  24. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/__main__.py +0 -0
  25. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  26. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  27. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  28. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  29. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  30. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/editor.js +0 -0
  31. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  32. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  33. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  34. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/render.js +0 -0
  35. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/assets/style.css +0 -0
  36. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/cli.py +0 -0
  37. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/export.py +0 -0
  38. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/jsrt.py +0 -0
  39. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/layout.py +0 -0
  40. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/measure.py +0 -0
  41. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/model.py +0 -0
  42. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/parser.py +0 -0
  43. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/src/quickdbd_cli/project.py +0 -0
  44. {quickdbd_cli-0.1.0 → quickdbd_cli-0.1.1}/tests/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.1.0
3
+ Version: 0.1.1
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
@@ -148,6 +148,17 @@ python scripts/gen_font_metrics.py # regenerate font width table
148
148
 
149
149
  Design notes: `docs/superpowers/specs/2026-10-17-qdbd-design.md`.
150
150
 
151
+ ### Releasing
152
+
153
+ Put PyPI credentials in `.env` (gitignored):
154
+
155
+ ```
156
+ TWINE_USERNAME=__token__
157
+ TWINE_PASSWORD=pypi-...
158
+ ```
159
+
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
+
151
162
  ## License
152
163
 
153
164
  Apache-2.0. Bundled third-party files are listed in [NOTICE](NOTICE).
@@ -123,6 +123,17 @@ python scripts/gen_font_metrics.py # regenerate font width table
123
123
 
124
124
  Design notes: `docs/superpowers/specs/2026-10-17-qdbd-design.md`.
125
125
 
126
+ ### Releasing
127
+
128
+ Put PyPI credentials in `.env` (gitignored):
129
+
130
+ ```
131
+ TWINE_USERNAME=__token__
132
+ TWINE_PASSWORD=pypi-...
133
+ ```
134
+
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
+
126
137
  ## License
127
138
 
128
139
  Apache-2.0. Bundled third-party files are listed in [NOTICE](NOTICE).
@@ -29,8 +29,8 @@
29
29
  "y": 5795
30
30
  },
31
31
  "Agency": {
32
- "x": 573,
33
- "y": 3287
32
+ "x": 603,
33
+ "y": 3752
34
34
  },
35
35
  "Allocation": {
36
36
  "x": 1655,
@@ -245,8 +245,8 @@
245
245
  "y": 5393
246
246
  },
247
247
  "EmailOpen": {
248
- "x": 3473,
249
- "y": 2776
248
+ "x": 3395,
249
+ "y": 2767
250
250
  },
251
251
  "EmailSend": {
252
252
  "x": 1927,
@@ -325,8 +325,8 @@
325
325
  "y": 4608
326
326
  },
327
327
  "Holiday": {
328
- "x": 3334,
329
- "y": 3050
328
+ "x": 3280,
329
+ "y": 3035
330
330
  },
331
331
  "Impression": {
332
332
  "x": 2544,
@@ -393,8 +393,8 @@
393
393
  "y": 6657
394
394
  },
395
395
  "Ledger": {
396
- "x": 3583,
397
- "y": 2235
396
+ "x": 3541,
397
+ "y": 2427
398
398
  },
399
399
  "LedgerEntry": {
400
400
  "x": 2512,
@@ -573,16 +573,16 @@
573
573
  "y": 3695
574
574
  },
575
575
  "Reseller": {
576
- "x": 3051,
577
- "y": 2797
576
+ "x": 3043,
577
+ "y": 2791
578
578
  },
579
579
  "Reservation": {
580
580
  "x": 3614,
581
581
  "y": 1303
582
582
  },
583
583
  "Return": {
584
- "x": 1231,
585
- "y": 3991
584
+ "x": 1113,
585
+ "y": 3858
586
586
  },
587
587
  "Review": {
588
588
  "x": 604,
@@ -761,8 +761,8 @@
761
761
  "y": 5153
762
762
  },
763
763
  "hr.Training": {
764
- "x": 1115,
765
- "y": 4126
764
+ "x": 1357,
765
+ "y": 3813
766
766
  },
767
767
  "inventory.Container": {
768
768
  "x": 2503,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quickdbd-cli"
7
- version = "0.1.0"
7
+ version = "0.1.1"
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,23 @@
1
+ #!/usr/bin/env bash
2
+ # Build and upload the current version to PyPI using credentials from .env,
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
5
+ set -euo pipefail
6
+ cd "$(dirname "$0")/.."
7
+
8
+ [ -f .env ] || { echo "error: .env with TWINE_USERNAME/TWINE_PASSWORD not found" >&2; exit 1; }
9
+ set -a; . ./.env; set +a
10
+
11
+ version=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
12
+ [ -z "$(git status --porcelain --untracked-files=no)" ] || { echo "error: uncommitted changes" >&2; exit 1; }
13
+
14
+ rm -rf dist
15
+ python3 -m pip install -q --upgrade build twine
16
+ python3 -m build
17
+ python3 -m twine check dist/*
18
+ python3 -m twine upload --non-interactive dist/*
19
+
20
+ git tag -a "v$version" -m "qdbd v$version"
21
+ git push origin "v$version"
22
+ gh release create "v$version" dist/* --title "qdbd v$version" --generate-notes
23
+ echo "Released v$version: https://pypi.org/project/quickdbd-cli/$version/"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -288,7 +288,7 @@
288
288
  }
289
289
  function exportSvg() {
290
290
  var only = S.focus ? Object.keys(neighbours(S.focus)) : null;
291
- return Q.renderSvg(S.model, S.pos, { mode: S.mode, only: only, fontCss: data.fontCss });
291
+ return Q.renderSvg(S.model, S.pos, { mode: S.mode, only: only, fontCss: document.getElementById("qdbd-font").textContent });
292
292
  }
293
293
  document.getElementById("xsvg").onclick = function () {
294
294
  download((data.title || "diagram") + ".svg", new Blob([exportSvg()], { type: "image/svg+xml" }));
@@ -67,7 +67,7 @@ SIDE = """
67
67
 
68
68
 
69
69
  def render_page(model: dict, positions: dict, title: str, serve: bool = False) -> str:
70
- data = {"model": model, "positions": positions, "serve": serve, "title": title, "fontCss": font_css()}
70
+ data = {"model": model, "positions": positions, "serve": serve, "title": title}
71
71
  hint = (
72
72
  "Drag tables to move them (saved). Click a table to focus. Esc clears."
73
73
  if serve
@@ -88,7 +88,10 @@ def render_page(model: dict, positions: dict, title: str, serve: bool = False) -
88
88
  '<meta name="viewport" content="width=device-width, initial-scale=1">'
89
89
  '<meta name="generator" content="qdbd">'
90
90
  f"<title>{_escape(title)}</title>"
91
- f"<style>{font_css()}{_asset('style.css')}</style>{head_extra}</head><body>"
91
+ # The font CSS lives in its own <style> so the viewer can reuse it for SVG/PNG export
92
+ # instead of embedding the fonts a second time.
93
+ f'<style id="qdbd-font">{font_css()}</style>'
94
+ f"<style>{_asset('style.css')}</style>{head_extra}</head><body>"
92
95
  f"{body}"
93
96
  f'<script type="application/json" id="qdbd-data">{_json_for_script(data)}</script>'
94
97
  f"<script>{_asset('render.js')}</script>"
@@ -178,13 +178,30 @@ def _handler(state: State, title: str):
178
178
  return Handler
179
179
 
180
180
 
181
- def serve(project: Project, port: int = 8765, open_browser: bool = True) -> int:
181
+ def make_server(project: Project, port: int = 8765) -> tuple[ThreadingHTTPServer, State]:
182
+ """Bind the HTTP server (port 0 picks a free port) without starting it."""
182
183
  state = State(project)
183
- for d in state.diagnostics:
184
- print(d["text"], file=sys.stderr)
185
184
  title = project.inputs[0].stem if len(project.inputs) == 1 else "diagram"
186
185
  server = ThreadingHTTPServer(("127.0.0.1", port), _handler(state, title))
187
186
  server.daemon_threads = True
187
+ return server, state
188
+
189
+
190
+ def poll_once(state: State) -> bool:
191
+ """Reload if any schema file changed; returns True when a reload happened."""
192
+ if not state.changed():
193
+ return False
194
+ try:
195
+ state.reload()
196
+ except Exception as e: # keep serving on unexpected errors
197
+ print(f"[qdbd] reload failed: {e}", file=sys.stderr, flush=True)
198
+ return True
199
+
200
+
201
+ def serve(project: Project, port: int = 8765, open_browser: bool = True) -> int:
202
+ server, state = make_server(project, port)
203
+ for d in state.diagnostics:
204
+ print(d["text"], file=sys.stderr)
188
205
  url = f"http://127.0.0.1:{server.server_address[1]}/"
189
206
  print(f"[qdbd] serving {len(state.files)} file(s) at {url} (layout: {display_path(project.sidecar)})")
190
207
  print("[qdbd] Ctrl+C to stop", flush=True)
@@ -192,11 +209,7 @@ def serve(project: Project, port: int = 8765, open_browser: bool = True) -> int:
192
209
  def watch() -> None:
193
210
  while True:
194
211
  time.sleep(0.3)
195
- if state.changed():
196
- try:
197
- state.reload()
198
- except Exception as e: # keep serving on unexpected errors
199
- print(f"[qdbd] reload failed: {e}", file=sys.stderr, flush=True)
212
+ poll_once(state)
200
213
 
201
214
  threading.Thread(target=watch, daemon=True).start()
202
215
  if open_browser:
@@ -8,6 +8,7 @@ import pytest
8
8
 
9
9
  from quickdbd_cli import export, layout
10
10
  from quickdbd_cli.cli import main
11
+ from quickdbd_cli.html import render_page
11
12
  from quickdbd_cli.measure import model_json, text_width
12
13
  from quickdbd_cli.parser import compile_text
13
14
  from quickdbd_cli.project import Project
@@ -138,6 +139,7 @@ def test_check_strict_warnings(tmp_path):
138
139
  def test_build_is_deterministic_and_does_not_write_sidecar(tmp_path):
139
140
  work = tmp_path / "s"
140
141
  shutil.copytree(SYNTH, work)
142
+ (work / "qdbd.layout.json").unlink(missing_ok=True)
141
143
  a, b = tmp_path / "a.html", tmp_path / "b.html"
142
144
  assert main(["build", str(work), "-o", str(a)]) == 0
143
145
  assert main(["build", str(work), "-o", str(b)]) == 0
@@ -159,6 +161,24 @@ def test_layout_command_writes_sidecar_and_reset(tmp_path):
159
161
  assert json.loads(side.read_text())["tables"]["Customer"] != {"x": 9999, "y": 9999}
160
162
 
161
163
 
164
+ def test_build_uses_but_never_rewrites_committed_sidecar(tmp_path):
165
+ work = tmp_path / "s"
166
+ shutil.copytree(SYNTH, work)
167
+ side = work / "qdbd.layout.json"
168
+ before = side.read_bytes()
169
+ assert main(["build", str(work), "-o", str(tmp_path / "d.html")]) == 0
170
+ assert side.read_bytes() == before
171
+ html = (tmp_path / "d.html").read_text()
172
+ customer = layout.read_sidecar(side)["Customer"]
173
+ assert f'"Customer":[{customer[0]},{customer[1]}]' in html
174
+
175
+
176
+ def test_html_embeds_font_once():
177
+ html = render_page(model_json(compile_text(SMALL)), {"Customer": [0, 0], "Order": [300, 0]}, "t")
178
+ assert html.count("@font-face") == 2 # regular + bold, once each
179
+ assert 'id="qdbd-font"' in html
180
+
181
+
162
182
  def test_missing_path(capsys):
163
183
  assert main(["check", "nope/does-not-exist.txt"]) == 2
164
184
 
@@ -0,0 +1,160 @@
1
+ """`qdbd serve` HTTP endpoints, driven without a browser."""
2
+
3
+ import json
4
+ import os
5
+ import threading
6
+ import time
7
+ import urllib.error
8
+ import urllib.request
9
+
10
+ import pytest
11
+
12
+ from quickdbd_cli import layout
13
+ from quickdbd_cli.project import Project, display_path
14
+ from quickdbd_cli.serve import make_server, poll_once
15
+
16
+ SCHEMA = """Customer
17
+ -
18
+ CustomerId PK int
19
+ Name string
20
+
21
+ Order
22
+ -
23
+ OrderId PK int
24
+ CustomerId int FK >- Customer.CustomerId
25
+ """
26
+
27
+
28
+ @pytest.fixture
29
+ def srv(tmp_path):
30
+ f = tmp_path / "shop.txt"
31
+ f.write_text(SCHEMA)
32
+ server, state = make_server(Project.open([f]), port=0)
33
+ t = threading.Thread(target=server.serve_forever, daemon=True)
34
+ t.start()
35
+ base = f"http://127.0.0.1:{server.server_address[1]}"
36
+ yield base, state, f
37
+ server.shutdown()
38
+ server.server_close()
39
+
40
+
41
+ def call(base, path, method="GET", body=None):
42
+ data = json.dumps(body).encode() if body is not None else None
43
+ req = urllib.request.Request(base + path, data=data, method=method, headers={"Content-Type": "application/json"})
44
+ try:
45
+ with urllib.request.urlopen(req, timeout=10) as r:
46
+ raw = r.read()
47
+ return r.status, (json.loads(raw) if r.headers.get_content_type() == "application/json" else raw)
48
+ except urllib.error.HTTPError as e:
49
+ return e.code, json.loads(e.read())
50
+
51
+
52
+ def bump_mtime(path):
53
+ st = path.stat()
54
+ os.utime(path, ns=(st.st_atime_ns, st.st_mtime_ns + 1_000_000_000))
55
+
56
+
57
+ def test_page_and_state(srv):
58
+ base, _, f = srv
59
+ code, page = call(base, "/")
60
+ assert code == 200 and b'id="side"' in page and b'"serve":true' in page
61
+ code, st = call(base, "/state")
62
+ assert code == 200
63
+ assert [t["name"] for t in st["model"]["tables"]] == ["Customer", "Order"]
64
+ assert st["files"] == [{"path": display_path(f), "text": SCHEMA, "mtime": st["files"][0]["mtime"]}]
65
+ assert isinstance(st["files"][0]["mtime"], str) # ns mtimes overflow JS numbers
66
+ assert st["diagnostics"] == []
67
+
68
+
69
+ def test_initial_layout_written_to_sidecar(srv):
70
+ _, state, f = srv
71
+ side = layout.read_sidecar(f.with_name("shop.layout.json"))
72
+ assert set(side) == {"Customer", "Order"}
73
+
74
+
75
+ def test_put_file_saves_and_returns_new_mtime(srv):
76
+ base, _, f = srv
77
+ _, st = call(base, "/state")
78
+ path, mtime = st["files"][0]["path"], st["files"][0]["mtime"]
79
+ new = SCHEMA + "\nProduct\n-\nProductId PK int\n"
80
+ code, res = call(base, "/file", "PUT", {"path": path, "text": new, "mtime": mtime})
81
+ assert code == 200 and res["mtime"] != mtime
82
+ assert f.read_text() == new
83
+
84
+
85
+ def test_put_file_refuses_stale_write(srv):
86
+ base, _, f = srv
87
+ _, st = call(base, "/state")
88
+ path, mtime = st["files"][0]["path"], st["files"][0]["mtime"]
89
+ f.write_text(SCHEMA + "\n# edited elsewhere\n")
90
+ bump_mtime(f)
91
+ code, res = call(base, "/file", "PUT", {"path": path, "text": "clobber", "mtime": mtime})
92
+ assert code == 409
93
+ assert "# edited elsewhere" in res["text"]
94
+ assert "# edited elsewhere" in f.read_text() # disk untouched
95
+
96
+
97
+ def test_put_unknown_file(srv):
98
+ base, _, _ = srv
99
+ code, _ = call(base, "/file", "PUT", {"path": "nope.txt", "text": "x", "mtime": None})
100
+ assert code == 404
101
+
102
+
103
+ def test_post_layout_updates_sidecar(srv):
104
+ base, state, f = srv
105
+ code, _ = call(base, "/layout", "POST", {"Customer": [1234, 567], "Unknown": [1, 1]})
106
+ assert code == 200
107
+ side = layout.read_sidecar(f.with_name("shop.layout.json"))
108
+ assert side["Customer"] == (1234, 567)
109
+ assert "Unknown" not in side
110
+ _, st = call(base, "/state")
111
+ assert st["positions"]["Customer"] == [1234, 567]
112
+
113
+
114
+ def test_external_edit_reloads_and_places_new_table(srv):
115
+ base, state, f = srv
116
+ _, before = call(base, "/state")
117
+ f.write_text(SCHEMA + "\nLine\n-\nLineId PK int\nOrderId int FK >- Order.OrderId\n")
118
+ bump_mtime(f)
119
+ assert poll_once(state)
120
+ _, after = call(base, "/state")
121
+ assert after["version"] > before["version"]
122
+ assert "Line" in after["positions"]
123
+ # existing tables did not move
124
+ for t in ("Customer", "Order"):
125
+ assert after["positions"][t] == before["positions"][t]
126
+ assert not poll_once(state) # nothing changed since
127
+
128
+
129
+ def test_errors_keep_last_good_model(srv):
130
+ base, state, f = srv
131
+ f.write_text(SCHEMA + "\nBad\n-\nX int FK >- Nope.Id\n")
132
+ bump_mtime(f)
133
+ poll_once(state)
134
+ _, st = call(base, "/state")
135
+ assert [d["severity"] for d in st["diagnostics"]] == ["error"]
136
+ assert "Invalid relationship, the table 'Nope' does not exist" in st["diagnostics"][0]["text"]
137
+ assert [t["name"] for t in st["model"]["tables"]] == ["Customer", "Order"]
138
+
139
+
140
+ def test_events_stream_announces_versions(srv):
141
+ base, state, f = srv
142
+ got = []
143
+
144
+ def listen():
145
+ with urllib.request.urlopen(base + "/events", timeout=10) as r:
146
+ for line in r:
147
+ line = line.decode().strip()
148
+ if line.startswith("data:"):
149
+ got.append(int(line[5:]))
150
+ if len(got) == 2:
151
+ return
152
+
153
+ t = threading.Thread(target=listen, daemon=True)
154
+ t.start()
155
+ time.sleep(0.3)
156
+ f.write_text(SCHEMA + "\n# touch\n")
157
+ bump_mtime(f)
158
+ poll_once(state)
159
+ t.join(timeout=10)
160
+ assert len(got) == 2 and got[1] > got[0]
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"
File without changes
File without changes
File without changes