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.
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/CHANGELOG.md +10 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/PKG-INFO +1 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/pyproject.toml +1 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/cli.py +10 -3
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/home.py +6 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/types.py +23 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/project.py +1 -1
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/answer.css +2 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/answer.html +13 -3
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/explore.js +5 -2
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/.gitignore +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/LICENSE +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/README.md +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/answer.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/answer_page.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/cache.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/ci.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/ci_signature.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/doctor.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/budget.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/column_lineage.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/column_ref.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/cte_tracing.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/dependencies.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/dialect.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/dispatch.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/extraction.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/jinja.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/json_sources.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/macro_source.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/pipeline.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/preprocess.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/safe_gen.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/schema_qualification.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/scope.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/select_sources.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/sql_script.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/statement.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tech_debt.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_catalog.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_scalar_vars.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_tvf.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/tsql_xml.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/types.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/unused_deps.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/engine/validation.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/graph.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/dbt.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/dbt_config.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/identity.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/sidecar.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/loaders/sqldir.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/lookml.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/mcp_server.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/names.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/py.typed +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/render.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/render_shims.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/schemas.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/semantic.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/server.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/sourcefiles.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/star_resolution.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/static/answer_twin.js +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/__init__.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/cli.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/collect.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/discover.py +0 -0
- {ripple_sql-0.1.0 → ripple_sql-0.1.1}/src/ripple/usage/ingest.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|