ripple-sql 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.

Potentially problematic release.


This version of ripple-sql might be problematic. Click here for more details.

Files changed (73) hide show
  1. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/CHANGELOG.md +10 -1
  2. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/PKG-INFO +1 -1
  3. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/pyproject.toml +1 -1
  4. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/cli.py +10 -3
  5. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/home.py +6 -1
  6. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/types.py +23 -0
  7. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/project.py +1 -1
  8. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/answer.css +2 -0
  9. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/answer.html +13 -3
  10. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/explore.js +5 -2
  11. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/.gitignore +0 -0
  12. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/LICENSE +0 -0
  13. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/README.md +0 -0
  14. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/__init__.py +0 -0
  15. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/answer.py +0 -0
  16. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/answer_page.py +0 -0
  17. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/cache.py +0 -0
  18. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/ci.py +0 -0
  19. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/ci_signature.py +0 -0
  20. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/doctor.py +0 -0
  21. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/__init__.py +0 -0
  22. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/budget.py +0 -0
  23. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/column_lineage.py +0 -0
  24. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/column_ref.py +0 -0
  25. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/cte_tracing.py +0 -0
  26. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/dependencies.py +0 -0
  27. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/dialect.py +0 -0
  28. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/dispatch.py +0 -0
  29. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/extraction.py +0 -0
  30. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/jinja.py +0 -0
  31. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/json_sources.py +0 -0
  32. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/macro_source.py +0 -0
  33. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/pipeline.py +0 -0
  34. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/preprocess.py +0 -0
  35. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/safe_gen.py +0 -0
  36. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/schema_qualification.py +0 -0
  37. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/scope.py +0 -0
  38. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/select_sources.py +0 -0
  39. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/sql_script.py +0 -0
  40. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/statement.py +0 -0
  41. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tech_debt.py +0 -0
  42. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_catalog.py +0 -0
  43. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_scalar_vars.py +0 -0
  44. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_tvf.py +0 -0
  45. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_xml.py +0 -0
  46. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/types.py +0 -0
  47. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/unused_deps.py +0 -0
  48. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/validation.py +0 -0
  49. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/graph.py +0 -0
  50. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/__init__.py +0 -0
  51. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/dbt.py +0 -0
  52. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/dbt_config.py +0 -0
  53. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/identity.py +0 -0
  54. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/sidecar.py +0 -0
  55. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/sqldir.py +0 -0
  56. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/lookml.py +0 -0
  57. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/mcp_server.py +0 -0
  58. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/names.py +0 -0
  59. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/py.typed +0 -0
  60. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/render.py +0 -0
  61. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/render_shims.py +0 -0
  62. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/schemas.py +0 -0
  63. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/semantic.py +0 -0
  64. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/server.py +0 -0
  65. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/sourcefiles.py +0 -0
  66. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/star_resolution.py +0 -0
  67. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/answer_twin.js +0 -0
  68. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/__init__.py +0 -0
  69. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/cli.py +0 -0
  70. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/collect.py +0 -0
  71. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/discover.py +0 -0
  72. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/ingest.py +0 -0
  73. {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/report.py +0 -0
@@ -6,6 +6,14 @@ Notable changes to Ripple. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.1] - 2026-09-09
10
+
11
+ ### Changed
12
+ - The map's home page says what it is for in one line ("Pick a column. See what breaks if it changes, or where it comes from.") and offers one example column to try; the five most-read columns come back with an answer. A source table's chip no longer shows a count, since the number was the columns some model happened to name, not the table; opening one says the list is not declared in the project and points at the Coverage rung that adds it.
13
+
14
+ ### Fixed
15
+ - Run from a home directory or the root of a disk, ripple refuses in one sentence instead of reading every SQL file the user owns. Progress lines ("scanning ...") reach a pipe as they are printed, not when the process exits.
16
+
9
17
  ## [0.1.0] - 2026-09-08
10
18
 
11
19
  First public release.
@@ -222,5 +230,6 @@ First public release.
222
230
  ### Removed
223
231
  - The dark whole-graph canvas that `ripple serve` used to open (`static/index.html`). Every surface now draws one answer.
224
232
 
225
- [Unreleased]: https://github.com/bteh/ripple/compare/v0.1.0...HEAD
233
+ [Unreleased]: https://github.com/bteh/ripple/compare/v0.1.1...HEAD
234
+ [0.1.1]: https://github.com/bteh/ripple/releases/tag/v0.1.1
226
235
  [0.1.0]: https://github.com/bteh/ripple/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ripple-sql
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Offline column-level SQL lineage. See what breaks before you merge.
5
5
  Project-URL: Homepage, https://github.com/bteh/ripple
6
6
  Project-URL: Repository, https://github.com/bteh/ripple
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ripple-sql"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  description = "Offline column-level SQL lineage. See what breaks before you merge."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -570,10 +570,12 @@ def utf8_streams() -> None:
570
570
  for stream in (sys.stdin, sys.stdout, sys.stderr):
571
571
  if stream is None or not hasattr(stream, "reconfigure"):
572
572
  continue
573
+ # line buffered either way: "scanning ..." must show while the scan
574
+ # runs, not when the process exits, even through a pipe
573
575
  if stream.isatty():
574
- stream.reconfigure(errors="replace")
576
+ stream.reconfigure(errors="replace", line_buffering=True)
575
577
  else:
576
- stream.reconfigure(encoding="utf-8", errors="replace")
578
+ stream.reconfigure(encoding="utf-8", errors="replace", line_buffering=True)
577
579
 
578
580
 
579
581
  def main(argv: list[str] | None = None) -> None:
@@ -726,7 +728,12 @@ def main(argv: list[str] | None = None) -> None:
726
728
  "doctor": cmd_doctor,
727
729
  "serve": cmd_serve,
728
730
  }
729
- handlers[args.command](args)
731
+ from ripple.loaders.types import NotAProject
732
+
733
+ try:
734
+ handlers[args.command](args)
735
+ except NotAProject as e:
736
+ sys.exit(str(e))
730
737
 
731
738
 
732
739
  if __name__ == "__main__":
@@ -130,10 +130,14 @@ def model_map(nodes: list[dict], edges) -> dict:
130
130
  members: dict[str, dict] = {}
131
131
  for n in nodes:
132
132
  source = n["type"] == "source"
133
+ columns = [c for c in n.get("columns") or [] if _is_column(c)]
133
134
  members[n["name"]] = {
134
135
  "id": n["name"],
135
136
  "kind": "source" if source else "model",
136
- "columns": [c for c in n.get("columns") or [] if _is_column(c)],
137
+ "columns": columns,
138
+ # a source with no declared list only shows the columns some
139
+ # model happens to name; the page must not present that as the table
140
+ "declared": bool(columns),
137
141
  "status": None if source else n.get("status"),
138
142
  "path": None if source else n.get("path"),
139
143
  }
@@ -156,6 +160,7 @@ def model_map(nodes: list[dict], edges) -> dict:
156
160
  "id": name,
157
161
  "kind": kind,
158
162
  "columns": sorted(seen.get(name, {})),
163
+ "declared": kind == "dashboard",
159
164
  "status": None,
160
165
  "path": None,
161
166
  }
@@ -169,6 +169,16 @@ def find_project_root(start: Path) -> Path:
169
169
  for candidate in [current, *current.parents]:
170
170
  if (candidate / "dbt_project.yml").exists():
171
171
  return candidate
172
+ # every caller passes through here before it stamps the cache or reads
173
+ # a file, so this is where a whole machine is refused
174
+ if whole_machine(current):
175
+ where = (
176
+ "your home directory" if current == Path.home().resolve() else "the root of the disk"
177
+ )
178
+ raise NotAProject(
179
+ f"{current} is {where}, not a project; run ripple inside a project folder, "
180
+ "or pass the folder with --path"
181
+ )
172
182
  return current
173
183
 
174
184
 
@@ -195,3 +205,16 @@ def relative_posix(path, root) -> str:
195
205
  OS, so a model's path is the same string in a PR comment written on
196
206
  Linux and on the laptop that reads it."""
197
207
  return path.relative_to(root).as_posix()
208
+
209
+
210
+ class NotAProject(ValueError):
211
+ """The path is a whole machine, not a project: reading it would crawl
212
+ everything under the home directory or the disk."""
213
+
214
+
215
+ def whole_machine(path: Path) -> bool:
216
+ """True for the home directory and for the root of a disk. A plain-SQL
217
+ scan starts wherever the command runs, and from either of those it
218
+ would read every file the user owns."""
219
+ here = Path(path).resolve()
220
+ return here == Path.home().resolve() or here == Path(here.anchor)
@@ -49,7 +49,7 @@ __all__ = [
49
49
 
50
50
  def load_project(path: str | Path = ".", dialect: str | None = None) -> Project:
51
51
  start = Path(path).resolve()
52
- root = find_project_root(start)
52
+ root = find_project_root(start) # refuses a home directory or a disk root
53
53
  # If we're standing above several dbt projects (a monorepo), gather them
54
54
  # all. find_project_root only walks UP to the nearest one, so scan DOWN.
55
55
  scan_root = root if (root / "dbt_project.yml").exists() else start
@@ -24,6 +24,7 @@ h1 code{font:500 24px/1 var(--mono); letter-spacing:0}
24
24
  .sum b{color:var(--ink); font-weight:600}
25
25
  .sum > span{position:relative}
26
26
  .sum > span + span::before{content:""; position:absolute; left:-9px; top:.3em; height:.9em; border-left:1px solid var(--line-strong)}
27
+ .lede{margin:14px 0 0; font-size:16px; color:var(--ink); text-wrap:pretty}
27
28
 
28
29
  .ask{display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin:28px 0 0; padding:16px 0 14px; border-top:1px solid var(--line); border-bottom:1px solid var(--line)}
29
30
  .ask input,.ask select{font:400 14px var(--mono); padding:8px 10px; border:1px solid var(--line-strong); border-radius:var(--r); background:var(--surface); color:var(--ink); min-width:0; height:36px}
@@ -105,6 +106,7 @@ pre{margin:0; padding:16px 22px 18px; overflow-x:auto; font:400 13px/1.65 var(--
105
106
  .cols{transition:opacity .18s var(--ease)}
106
107
  .wires path.on{stroke:var(--accent); stroke-width:1.5}
107
108
  .cols{display:flex; flex-direction:column; gap:5px; margin:0 0 6px 10px; padding-left:10px; border-left:1px solid var(--line)}
109
+ .cols .gname{max-width:36ch; white-space:normal; line-height:1.45; padding-top:2px}
108
110
  .cols .node{cursor:pointer}
109
111
  .cols .node:hover{border-color:var(--accent); color:var(--accent-strong)}
110
112
  .more-chips{font:500 12.5px var(--sans); color:var(--accent); background:none; border:0; padding:4px 0; cursor:pointer; text-align:left}
@@ -13,6 +13,7 @@
13
13
  <p class="eyebrow"><span class="mark" aria-hidden="true"></span><span id="eyebrow">Ripple</span></p>
14
14
  <h1 id="title"></h1>
15
15
  <p class="sum" id="sum"></p>
16
+ <p class="lede" id="lede" hidden>Pick a column. See what breaks if it changes, or where it comes from.</p>
16
17
 
17
18
  <div class="change" id="change" hidden><span class="lab">In this change</span></div>
18
19
 
@@ -77,7 +78,8 @@
77
78
  demo.hidden = false;
78
79
  mapEl.hidden = true;
79
80
  nav.hidden = !mapApi;
80
- if (mapApi && L.starters.length) document.getElementById('starters').hidden = false;
81
+ document.getElementById('lede').hidden = true;
82
+ if (mapApi) starterStrip(true);
81
83
  document.getElementById('eyebrow').textContent = 'Ripple · ' + (DATA.change ? 'what this change breaks' : (a.kind === 'trace' ? 'where it comes from' : 'what breaks'));
82
84
  document.getElementById('title').innerHTML = a.kind === 'trace'
83
85
  ? 'Where does <code>' + esc(a.target) + '</code> come from?'
@@ -185,6 +187,14 @@
185
187
 
186
188
  // ---- the home state: ripple serve opens here, on the whole project ----
187
189
  var P = DATA.project || {}, L = DATA.live;
190
+ // one example on the map ("Try"), the five most-read columns with an answer
191
+ function starterStrip(all){
192
+ var strip = document.getElementById('starters');
193
+ if (!L || !L.starters.length) return;
194
+ strip.hidden = false;
195
+ strip.querySelector('.lab').textContent = all ? 'Start with' : 'Try';
196
+ Array.prototype.slice.call(strip.querySelectorAll('button')).forEach(function(b, i){ b.hidden = !all && i > 0; });
197
+ }
188
198
  function home(){
189
199
  current = null;
190
200
  demo.hidden = true;
@@ -200,9 +210,9 @@
200
210
  if (L.review) bits.push('<b>' + L.review + ' need review</b>');
201
211
  document.getElementById('sum').innerHTML = bits.map(function(b){ return '<span>' + b + '</span>'; }).join('');
202
212
  document.title = 'Ripple: ' + (P.root || 'local');
213
+ document.getElementById('lede').hidden = !mapApi;
203
214
  if (mapApi){
204
- // the map is the place to start; the starter chips come back with an answer
205
- document.getElementById('starters').hidden = true;
215
+ starterStrip(false);
206
216
  mapEl.hidden = false;
207
217
  mapApi.filter(document.getElementById('q').value.trim());
208
218
  requestAnimationFrame(mapApi.draw);
@@ -47,7 +47,8 @@ function renderMap(root, map, opts){
47
47
  var broken = m.status && m.status !== 'ok' && m.status !== 'star_only';
48
48
  var b = el('button', 'mdl' + (m.kind === 'source' ? ' src' : '') + (m.kind === 'dashboard' ? ' bi' : '') + (broken ? ' r' : ''));
49
49
  b.type = 'button'; b.dataset.id = m.id; b.setAttribute('aria-expanded', 'false');
50
- b.innerHTML = label(m) + (m.columns.length ? ' <small>' + m.columns.length + '</small>' : '');
50
+ // a source's count would be the columns some model names, not the table's width
51
+ b.innerHTML = label(m) + (m.declared && m.kind !== 'source' && m.columns.length ? ' <small>' + m.columns.length + '</small>' : '');
51
52
  var reads = (ups[m.id] || []).length, feeds = (downs[m.id] || []).length;
52
53
  b.title = (m.path || m.id) + ' · reads ' + reads + ', feeds ' + feeds + (broken ? ' · ' + m.status.replace('_', ' ') : '');
53
54
  b.addEventListener('click', function(){ toggle(m, g, b); });
@@ -81,7 +82,9 @@ function renderMap(root, map, opts){
81
82
  });
82
83
  var note = null;
83
84
  if (m.columns.length > MAP_COLUMNS) note = '+ ' + (m.columns.length - MAP_COLUMNS) + ' more, ask by name';
84
- else if (!m.columns.length) note = m.kind === 'source' ? 'columns unknown, see Coverage' : 'no columns known';
85
+ else if (!m.declared && m.kind === 'source') note = (m.columns.length ? 'only the columns this project names; the full list is' : 'no column list; it is') +
86
+ ' not declared in the project (a select * reads it unnamed). Coverage below adds it.';
87
+ else if (!m.columns.length) note = 'no columns known';
85
88
  if (note){ var n = el('span', 'gname'); n.textContent = note; cols.appendChild(n); }
86
89
  g.appendChild(cols); b.setAttribute('aria-expanded', 'true'); draw();
87
90
  }
File without changes
File without changes
File without changes
File without changes