queryview 0.0.2__py3-none-any.whl

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 (54) hide show
  1. queryview/__init__.py +0 -0
  2. queryview/conftest.py +86 -0
  3. queryview/connect.py +428 -0
  4. queryview/dashboard_queries.py +75 -0
  5. queryview/dashboards.py +156 -0
  6. queryview/drivers/__init__.py +10 -0
  7. queryview/drivers/base.py +165 -0
  8. queryview/drivers/clickhouse.py +138 -0
  9. queryview/drivers/duckdb.py +153 -0
  10. queryview/drivers/postgres.py +166 -0
  11. queryview/drivers/test_base.py +49 -0
  12. queryview/drivers/test_clickhouse.py +53 -0
  13. queryview/drivers/test_contract.py +77 -0
  14. queryview/drivers/test_duckdb.py +80 -0
  15. queryview/drivers/test_postgres.py +80 -0
  16. queryview/gitsync.py +374 -0
  17. queryview/main.py +740 -0
  18. queryview/mcp_server.py +294 -0
  19. queryview/migrations/env.py +39 -0
  20. queryview/migrations/script.py.mako +29 -0
  21. queryview/migrations/versions/9a536b7c0328_initial_schema.py +89 -0
  22. queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +59 -0
  23. queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +32 -0
  24. queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +98 -0
  25. queryview/queries.py +159 -0
  26. queryview/remote.py +141 -0
  27. queryview/static/assets/index-CvnC_D68.js +47 -0
  28. queryview/static/assets/index-Qe7bhycG.css +2 -0
  29. queryview/static/favicon.svg +1 -0
  30. queryview/static/index.html +14 -0
  31. queryview/test_api_db.py +51 -0
  32. queryview/test_api_export_import.py +85 -0
  33. queryview/test_api_gitsync.py +83 -0
  34. queryview/test_api_workspaces.py +44 -0
  35. queryview/test_connect_flow.py +123 -0
  36. queryview/test_connect_store.py +34 -0
  37. queryview/test_dashboards.py +216 -0
  38. queryview/test_gitsync.py +346 -0
  39. queryview/test_main.py +18 -0
  40. queryview/test_mcp_gitsync.py +72 -0
  41. queryview/test_migrations.py +99 -0
  42. queryview/test_queries.py +170 -0
  43. queryview/test_remote.py +260 -0
  44. queryview/test_validation.py +87 -0
  45. queryview/test_workspaces.py +109 -0
  46. queryview/test_yamlio.py +198 -0
  47. queryview/validation.py +111 -0
  48. queryview/workspaces.py +167 -0
  49. queryview/yamlio.py +245 -0
  50. queryview-0.0.2.dist-info/METADATA +183 -0
  51. queryview-0.0.2.dist-info/RECORD +54 -0
  52. queryview-0.0.2.dist-info/WHEEL +4 -0
  53. queryview-0.0.2.dist-info/entry_points.txt +3 -0
  54. queryview-0.0.2.dist-info/licenses/LICENSE +21 -0
queryview/yamlio.py ADDED
@@ -0,0 +1,245 @@
1
+ """YAML export/import of predefined queries and dashboards — one entity at a
2
+ time or a whole workspace as a single bundle. Every exported document is
3
+ self-describing via a top-level `kind` (query | dashboard | workspace), so
4
+ import dispatches on file content, never on the filename. Import validates the
5
+ whole document before writing anything, then upserts (same overwrite semantics
6
+ as gitsync restore). This module also owns the entity <-> plain-mapping codec
7
+ and the YAML dumper that gitsync's per-entity repo files build on.
8
+ Docs: docs/export-import.md."""
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from typing import Any
14
+
15
+ import yaml
16
+
17
+ from .validation import cell_view_error, presentation_error
18
+ from .workspaces import WorkspaceRec
19
+
20
+
21
+ class YamlIOError(Exception):
22
+ """Export/import failure carrying an HTTP-ish status for the API layer:
23
+ 400 malformed document, 404 entity not found."""
24
+
25
+ def __init__(self, message: str, status: int = 400):
26
+ super().__init__(message)
27
+ self.status = status
28
+
29
+
30
+ # --- Dumper (also used by gitsync's per-entity files) -----------------------
31
+
32
+
33
+ def _repr_str(dumper: yaml.SafeDumper, data: str):
34
+ # Multiline strings (SQL, HTML, cell_view) as literal blocks for readable
35
+ # files and diffs.
36
+ style = "|" if "\n" in data else None
37
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=style)
38
+
39
+
40
+ class _Dumper(yaml.SafeDumper):
41
+ pass
42
+
43
+
44
+ _Dumper.add_representer(str, _repr_str)
45
+
46
+
47
+ def dump_yaml(data: Any) -> str:
48
+ return yaml.dump(data, Dumper=_Dumper, sort_keys=False, allow_unicode=True)
49
+
50
+
51
+ _SAFE = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._ -")
52
+
53
+
54
+ def slug(name: str) -> str:
55
+ """Filesystem-safe name: percent-encode (UTF-8) anything outside
56
+ [A-Za-z0-9._ -] plus a leading dot. Used for export download filenames and
57
+ gitsync repo paths; the canonical name lives inside the YAML — readers
58
+ trust the file content, never the path."""
59
+ out: list[str] = []
60
+ for i, ch in enumerate(name):
61
+ if ch in _SAFE and not (ch == "." and i == 0):
62
+ out.append(ch)
63
+ else:
64
+ out.append("".join(f"%{b:02X}" for b in ch.encode("utf-8")))
65
+ return "".join(out)
66
+
67
+
68
+ # --- Entity <-> mapping codec (shared with gitsync) -------------------------
69
+
70
+
71
+ def query_to_data(row: dict[str, Any]) -> dict[str, Any]:
72
+ """One predefined-query row (list_predefined_queries shape: order_by/fields
73
+ as JSON text) as its exported mapping. None/empty keys are omitted."""
74
+ data: dict[str, Any] = {"query_name": row["query_name"], "query": row["query"]}
75
+ if row.get("cell_view"):
76
+ data["cell_view"] = row["cell_view"]
77
+ if row.get("order_by"):
78
+ data["order_by"] = json.loads(row["order_by"])
79
+ if row.get("fields"):
80
+ data["fields"] = json.loads(row["fields"])
81
+ return data
82
+
83
+
84
+ def _require_str(data: dict[str, Any], key: str, where: str) -> str:
85
+ value = data.get(key)
86
+ if not isinstance(value, str) or not value.strip():
87
+ raise YamlIOError(f"malformed {where}: {key} must be a non-empty string")
88
+ return value
89
+
90
+
91
+ def query_from_data(data: Any, where: str = "query file") -> dict[str, Any]:
92
+ """Validate a query mapping back to the DB row shape save_predefined_query
93
+ takes (order_by/fields re-serialized to JSON text or None)."""
94
+ if not isinstance(data, dict):
95
+ raise YamlIOError(f"malformed {where}: expected a mapping")
96
+ query_name = _require_str(data, "query_name", where)
97
+ query = _require_str(data, "query", where)
98
+ cell_view = data.get("cell_view")
99
+ ob, fl = data.get("order_by"), data.get("fields")
100
+ perr = cell_view_error(cell_view) or presentation_error(ob, fl)
101
+ if perr is not None:
102
+ raise YamlIOError(f"malformed {where}: {perr}")
103
+ return {
104
+ "query_name": query_name,
105
+ "query": query,
106
+ "cell_view": cell_view or None,
107
+ "order_by": json.dumps(ob) if ob else None,
108
+ "fields": json.dumps(fl) if fl else None,
109
+ }
110
+
111
+
112
+ def dashboard_to_data(d: dict[str, Any]) -> dict[str, Any]:
113
+ """One dashboard (get_dashboard shape) as its exported mapping. Keys are
114
+ listed explicitly to pin the document format independently of what the
115
+ store accessors happen to return."""
116
+ return {
117
+ "name": d["name"],
118
+ "connection": d["connection"],
119
+ "html": d["html"],
120
+ "queries": d["queries"] or {},
121
+ }
122
+
123
+
124
+ def dashboard_from_data(data: Any, where: str = "dashboard file", require_html: bool = True) -> dict[str, Any]:
125
+ """Validate a dashboard mapping back to the upsert_dashboard shape.
126
+ gitsync passes require_html=False: its repo format stores the HTML as a
127
+ separate file that may legitimately be absent/empty."""
128
+ if not isinstance(data, dict):
129
+ raise YamlIOError(f"malformed {where}: expected a mapping")
130
+ name = _require_str(data, "name", where)
131
+ connection = _require_str(data, "connection", where)
132
+ html = _require_str(data, "html", where) if require_html else str(data.get("html") or "")
133
+ queries = data.get("queries") or {}
134
+ if not isinstance(queries, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in queries.items()):
135
+ raise YamlIOError(f"malformed {where}: queries must be a {{name: SQL}} map")
136
+ return {"name": name, "connection": connection, "html": html, "queries": queries}
137
+
138
+
139
+ # --- Import documents -------------------------------------------------------
140
+
141
+
142
+ def _parse_query_entry(data: Any, where: str = "query document") -> dict[str, Any]:
143
+ """A query import entry: the shared codec row plus the `type` key export
144
+ documents carry (gitsync files get it from the repo path instead)."""
145
+ if not isinstance(data, dict):
146
+ raise YamlIOError(f"malformed {where}: expected a mapping")
147
+ return {"type": _require_str(data, "type", where), **query_from_data(data, where)}
148
+
149
+
150
+ def parse_document(text: str) -> tuple[str, Any]:
151
+ """Parse + validate an import document. Returns (kind, payload): for
152
+ kind 'query'/'dashboard' the payload is one parsed entry; for 'workspace'
153
+ a {'queries': [...], 'dashboards': [...]} pair of parsed entries."""
154
+ try:
155
+ data = yaml.safe_load(text)
156
+ except yaml.YAMLError as e:
157
+ raise YamlIOError(f"malformed YAML: {e}") from e
158
+ if not isinstance(data, dict):
159
+ raise YamlIOError("malformed document: expected a YAML mapping")
160
+ kind = data.get("kind")
161
+ if kind == "query":
162
+ return "query", _parse_query_entry(data)
163
+ if kind == "dashboard":
164
+ return "dashboard", dashboard_from_data(data, "dashboard document")
165
+ if kind == "workspace":
166
+ raw_queries = data.get("queries") or []
167
+ raw_dashboards = data.get("dashboards") or []
168
+ if not isinstance(raw_queries, list) or not isinstance(raw_dashboards, list):
169
+ raise YamlIOError("malformed workspace document: queries and dashboards must be lists")
170
+ return "workspace", {
171
+ "queries": [_parse_query_entry(q, f"workspace queries[{i}]") for i, q in enumerate(raw_queries)],
172
+ "dashboards": [dashboard_from_data(d, f"workspace dashboards[{i}]") for i, d in enumerate(raw_dashboards)],
173
+ }
174
+ raise YamlIOError("malformed document: kind must be 'query', 'dashboard' or 'workspace'")
175
+
176
+
177
+ # --- Services ---------------------------------------------------------------
178
+
179
+
180
+ async def export(kind: str, name: str, conn_type: str, ws: WorkspaceRec) -> tuple[str, str]:
181
+ """Export dispatch shared by every caller (REST today, MCP/CLI tomorrow):
182
+ validates kind/name/conn_type, loads the document, and owns the
183
+ `{slug}.{kind}.yaml` download-filename convention. Returns (filename, text)."""
184
+ if kind not in ("query", "dashboard", "workspace") or (kind != "workspace" and not name):
185
+ raise YamlIOError("kind ('query'|'dashboard'|'workspace') and (for entities) name are required")
186
+ if kind == "query":
187
+ if not conn_type:
188
+ raise YamlIOError("conn_type is required for queries")
189
+ return f"{slug(name)}.query.yaml", await export_query(conn_type, name, ws.id)
190
+ if kind == "dashboard":
191
+ return f"{slug(name)}.dashboard.yaml", await export_dashboard(name, ws.id)
192
+ return f"{slug(ws.name)}.workspace.yaml", await export_workspace(ws.id)
193
+
194
+
195
+ async def export_query(conn_type: str, name: str, workspace_id: int) -> str:
196
+ from .queries import get_predefined_query
197
+
198
+ row = await get_predefined_query(conn_type, name, workspace_id)
199
+ if row is None:
200
+ raise YamlIOError(f"query {name!r} not found", status=404)
201
+ return dump_yaml({"kind": "query", "type": conn_type, **query_to_data(row)})
202
+
203
+
204
+ async def export_dashboard(name: str, workspace_id: int) -> str:
205
+ from .dashboards import get_dashboard
206
+
207
+ d = await get_dashboard(name, workspace_id)
208
+ if d is None:
209
+ raise YamlIOError(f"dashboard {name!r} not found", status=404)
210
+ return dump_yaml({"kind": "dashboard", **dashboard_to_data(d)})
211
+
212
+
213
+ async def export_workspace(workspace_id: int) -> str:
214
+ """The whole workspace (every predefined query and dashboard) as one
215
+ bundle. Empty workspaces export a valid, importable empty bundle."""
216
+ from .dashboards import list_dashboards_full
217
+ from .queries import list_all_predefined_queries
218
+
219
+ queries = await list_all_predefined_queries(workspace_id)
220
+ dashboards = await list_dashboards_full(workspace_id)
221
+ return dump_yaml(
222
+ {
223
+ "kind": "workspace",
224
+ "queries": [{"type": r["type"], **query_to_data(r)} for r in queries],
225
+ "dashboards": [dashboard_to_data(d) for d in dashboards],
226
+ }
227
+ )
228
+
229
+
230
+ async def import_text(text: str, workspace_id: int) -> dict[str, Any]:
231
+ """Import a document into a workspace, upserting by name. The whole
232
+ document is validated before the first write, so a malformed file changes
233
+ nothing; each entity family then writes in a single transaction. Returns
234
+ {'kind', 'queries', 'dashboards'} counts."""
235
+ from .dashboards import upsert_dashboards
236
+ from .queries import save_predefined_queries
237
+
238
+ kind, payload = parse_document(text)
239
+ queries = payload["queries"] if kind == "workspace" else [payload] if kind == "query" else []
240
+ dashboards = payload["dashboards"] if kind == "workspace" else [payload] if kind == "dashboard" else []
241
+ if queries:
242
+ await save_predefined_queries(queries, workspace_id=workspace_id)
243
+ if dashboards:
244
+ await upsert_dashboards(dashboards, workspace_id=workspace_id)
245
+ return {"kind": kind, "queries": len(queries), "dashboards": len(dashboards)}
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.5
2
+ Name: queryview
3
+ Version: 0.0.2
4
+ Summary: QueryView: FastAPI + SQLModel backend and React SPA over the ClickHouse HTTP interface
5
+ Project-URL: Homepage, https://github.com/kolodkin/queryview
6
+ Project-URL: Repository, https://github.com/kolodkin/queryview
7
+ Project-URL: Issues, https://github.com/kolodkin/queryview/issues
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: aiosqlite>=0.22.1
12
+ Requires-Dist: alembic>=1.13
13
+ Requires-Dist: asyncpg>=0.29
14
+ Requires-Dist: cryptography>=42
15
+ Requires-Dist: duckdb>=1.0
16
+ Requires-Dist: fastapi>=0.110
17
+ Requires-Dist: httpx>=0.27
18
+ Requires-Dist: mcp<2,>=1.9
19
+ Requires-Dist: pyyaml>=6
20
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
21
+ Requires-Dist: sqlmodel>=0.0.16
22
+ Requires-Dist: uvicorn>=0.29
23
+ Description-Content-Type: text/markdown
24
+
25
+ # QueryView
26
+
27
+ Project skeleton: **Python** backend (**FastAPI + SQLModel**) + **Vite + React + TypeScript** SPA frontend with **Tailwind CSS**, plus **[Playwright](https://playwright.dev)** end-to-end tests.
28
+
29
+ ## Quick start
30
+
31
+ Run the released package — API + bundled SPA on http://localhost:8000:
32
+
33
+ ```bash
34
+ uvx queryview
35
+ ```
36
+
37
+ `--port` (or the `PORT` env var) picks the listen port, default 8000:
38
+ `uvx queryview --port 9000`.
39
+
40
+ Or run the container image — every release publishes
41
+ `ghcr.io/kolodkin/queryview` to GHCR for `linux/amd64` and `linux/arm64`, tagged
42
+ `vX.Y.Z` and (for non-pre-releases) `latest`:
43
+
44
+ ```bash
45
+ docker run -p 8000:8000 ghcr.io/kolodkin/queryview:latest
46
+ ```
47
+
48
+ To serve on a different host port, remap it (the container keeps listening on
49
+ 8000, which its healthcheck probes): `docker run -p 9000:8000 ...`.
50
+
51
+ State (the SQLite DB and its encryption key) lives in `/home/queryview`; mount a
52
+ volume there to persist it across containers:
53
+ `docker run -p 8000:8000 -v queryview-data:/home/queryview ghcr.io/kolodkin/queryview:latest`.
54
+
55
+ ## Layout
56
+
57
+ ```
58
+ .
59
+ ├── backend/ # Python FastAPI + SQLModel app exposing /api/* (queryview package)
60
+ ├── frontend/ # Vite + React + TS + Tailwind v4 SPA (npm workspace)
61
+ ├── e2e/ # Playwright (pytest) browser tests
62
+ ├── pyproject.toml # Backend deps + console script + e2e `test` group (uv)
63
+ └── package.json # npm workspace root: dev orchestration + frontend build
64
+ ```
65
+
66
+ ## Prerequisites
67
+
68
+ - [uv](https://docs.astral.sh/uv/) — runs the Python backend and the Playwright
69
+ (pytest) e2e suite (it manages the Python toolchain and dependencies for you).
70
+ - [Node.js](https://nodejs.org) 20+ (with npm) — runs the root tasks and the
71
+ Vite frontend.
72
+
73
+ npm runs the frontend and the root task scripts; uv handles the backend's and
74
+ e2e suite's Python virtualenv and dependencies.
75
+
76
+ ## Install
77
+
78
+ Install the backend's Python dependencies (uv reads the root `pyproject.toml`;
79
+ the package lives in `backend/queryview`):
80
+
81
+ ```bash
82
+ uv sync
83
+ ```
84
+
85
+ Install the JavaScript dependencies for the frontend workspace:
86
+
87
+ ```bash
88
+ npm install
89
+ ```
90
+
91
+ Install the e2e tooling (the `test` dependency group) and fetch the Playwright
92
+ browser:
93
+
94
+ ```bash
95
+ uv sync --group test
96
+ uv run --group test playwright install chromium
97
+ ```
98
+
99
+ ## Run dev servers
100
+
101
+ Run backend and frontend together:
102
+
103
+ ```bash
104
+ npm run dev
105
+ ```
106
+
107
+ Or individually:
108
+
109
+ ```bash
110
+ npm run backend # uvicorn --reload on http://localhost:8000
111
+ npm run frontend # http://localhost:5173
112
+ ```
113
+
114
+ The Vite dev server proxies `/api/*` to the FastAPI backend, so the SPA can call the API on the same origin.
115
+
116
+ ## Build & preview production
117
+
118
+ ```bash
119
+ npm run build # produces frontend/dist/
120
+ npm run start # SERVE_STATIC=1, FastAPI serves dist/ + /api on :8000
121
+ npm run preview # build && start in one shot
122
+ ```
123
+
124
+ In production there is no Vite — the FastAPI backend serves the bundled SPA from `frontend/dist/` and falls back to `index.html` for any unknown non-`/api` path so client-side routing works. Override the dist location with `STATIC_ROOT=/path/to/dist`.
125
+
126
+ ## End-to-end tests
127
+
128
+ The e2e suite is [pytest-playwright](https://playwright.dev/python/docs/test-runners),
129
+ installed via the `test` dependency group and run through `uv`.
130
+
131
+ Start the dev servers (`npm run dev`) in one terminal, then in another:
132
+
133
+ ```bash
134
+ uv run --group test pytest
135
+ ```
136
+
137
+ Override the target URL with `BASE_URL=http://localhost:4173 uv run --group test pytest` (e.g. to test a built preview). To run the full suite against a real ClickHouse the way CI does, use `scripts/setup.sh`.
138
+
139
+ ## Release to PyPI
140
+
141
+ The **Publish to PyPI** workflow (`.github/workflows/publish.yaml`, manual
142
+ dispatch with a `vX.Y.Z` tag input) builds the SPA into the wheel
143
+ (`queryview/static/`), then gates the release on the installed wheel: an HTTP
144
+ smoke test, the packaged backend test suite (`pytest --pyargs queryview`), and
145
+ the Playwright e2e suite driving the packaged server (skippable via the
146
+ `skip-e2e` input for emergencies). It then publishes
147
+ [`queryview`](https://pypi.org/project/queryview/) via PyPI trusted publishing,
148
+ pushes the tag, and creates the GitHub release. The package version comes
149
+ from the tag (no version bump in `pyproject.toml`).
150
+
151
+ An installed wheel serves the bundled UI by default — see
152
+ [Quick start](#quick-start).
153
+
154
+ ## MCP server
155
+
156
+ The backend mounts a FastMCP server (Streamable HTTP) at
157
+ `http://localhost:8000/mcp`. There is nothing extra to start — it runs inside
158
+ the server process (`uvx queryview`, `npm run dev`, ...). Hook up an MCP client,
159
+ e.g.:
160
+
161
+ ```bash
162
+ claude mcp add --transport http queryview http://localhost:8000/mcp
163
+ ```
164
+
165
+ Tools: `run_query` (read-only SQL, rows returned to the agent), `push_query`
166
+ and `push_dashboard` (fill a live browser session), `list_queries` /
167
+ `list_dashboards`, and `git_store` / `git_history` / `git_restore` (workspace
168
+ git backups). The push tools target an **armed** browser session: enable
169
+ "Allow remote control" from the agent icon next to the connection pill and use
170
+ the session id it shows. See [docs/remote.md](docs/remote.md) for the full
171
+ protocol.
172
+
173
+ ## API
174
+
175
+ See [docs/api.md](docs/api.md) for the full endpoint reference.
176
+
177
+ The single-page prompt UI is described in [docs/queryview.md](docs/queryview.md);
178
+ connecting (`new <type>` / `connect <name>`), SQLite persistence, and session
179
+ auto-connect are specified in [docs/connect.md](docs/connect.md).
180
+
181
+ Connections are stored in SQLite (`backend/queryview.db`, override with
182
+ `DB_PATH`); the backend writes that file and a local password-encryption key
183
+ (`backend/queryview.db.key`, override with `DB_KEY_PATH`).
@@ -0,0 +1,54 @@
1
+ queryview/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ queryview/conftest.py,sha256=TJpnXRopAQufqXpCIizcC2p-eJML9I70gaji_3MxV_k,2918
3
+ queryview/connect.py,sha256=97r_XtBdLJ_-LqhERlxrepgP-dYYJ4_puF9Mv6S8oPQ,14724
4
+ queryview/dashboard_queries.py,sha256=Q2DbHSjIr8h9p5Y3HMo3oNNrbbfaiGW8colBCSD5Bh8,2770
5
+ queryview/dashboards.py,sha256=az92sJBYy6jBheyd186sqNoPVELRPoLGbmWyl0J_pQg,6075
6
+ queryview/gitsync.py,sha256=UCHNpJPWAFnOat2nGlgq_TPM_g64ZlYcxXAVMgiSp0o,14367
7
+ queryview/main.py,sha256=I_YyNRRAPgiFWqY_VvlolOayByWSRt4JLLoderrbDcI,27701
8
+ queryview/mcp_server.py,sha256=E8DhTrisom7xAXJ3F-qIQzrV-gVG4Qp5Sq_7wuBHpCY,11603
9
+ queryview/queries.py,sha256=isGYoUMShr1B9dXdwiozzqavRXfeFsIj9Mjs1U04b88,6148
10
+ queryview/remote.py,sha256=gdt5O_3x53M0ZP3Z-gnm3zi_Jx-0BbdJHoFhNlT_FBk,5090
11
+ queryview/test_api_db.py,sha256=6QOlyZK9OsrVm0foz7M32S4M2Qv_sdGjNHnjV3v4OC4,1897
12
+ queryview/test_api_export_import.py,sha256=EJ32EBBoN2EXbyV-v1H0E9AumPc2qzX1EU0893dbaJY,3690
13
+ queryview/test_api_gitsync.py,sha256=KtD3yxWQ7UyGV8S260N4Ez86l4oMg3NgQ5hxRrtVIfk,2731
14
+ queryview/test_api_workspaces.py,sha256=yyn6ib6s3B0TnTnLCAd1T1k5ev-kBGtkWtzINfaGpEU,1700
15
+ queryview/test_connect_flow.py,sha256=P37kTaGXDxXOAYXvR5LMbp7k2_XIacP4YcTnnIwpVT0,4454
16
+ queryview/test_connect_store.py,sha256=lxu3Yg5rQlza4HBWsvJ7LNJYo8dB0xD_DwXMNRkoUx4,1065
17
+ queryview/test_dashboards.py,sha256=ib9jJ-ueMF02LR3mvYIigSmdZ5N9wA_01Ww2UljskMo,8075
18
+ queryview/test_gitsync.py,sha256=T5j4AHxMMseIKsMvdsQqWrbKgJp8qTKcq8QoTGmR78s,13053
19
+ queryview/test_main.py,sha256=k5YuxmwfU0zemzbF2ktdCSiLes8vYUu7LPoW8gAiWHY,504
20
+ queryview/test_mcp_gitsync.py,sha256=UU1n0ic9jY-c4jDzlV-IUZO7j9SIVIMfwOGImzQMEL8,2328
21
+ queryview/test_migrations.py,sha256=QVPnQYBkOzLGGEowJxHeUzQVYcYlBwFq348TAzNKxLs,3380
22
+ queryview/test_queries.py,sha256=Mm264HqBzDKZ1A0aRf_ol8aSOA5hAyzSG7nC1Ji10rk,5694
23
+ queryview/test_remote.py,sha256=1p9J2yNdJ94rfk2rHqEZ32T4kJpaYcMEXZVVP_GNDoQ,7980
24
+ queryview/test_validation.py,sha256=HzxpThEQ31sH35-GJZ7xN0tuuWYgjUCXexuJxZXJoSU,3221
25
+ queryview/test_workspaces.py,sha256=uQig4cay59p_-EgHUJdn0OefLH1hAj-Vh21GHnSMt7k,3546
26
+ queryview/test_yamlio.py,sha256=3WxqkB2ACSYXP_Xpss6bD_cdBYR_FbZFgmFmfMFR3Wk,7325
27
+ queryview/validation.py,sha256=CtwKtRpvErdfJ4ruIC6Hlno85g_-11pBwD4JnNbjDOg,5098
28
+ queryview/workspaces.py,sha256=FsXWBT6iM3qC8Uql2MULZDyg2CLYfZzF0G6opjY6LZo,6182
29
+ queryview/yamlio.py,sha256=DyiiWuOWIkCGaMWu54GnPNoDEZ7T6lyCVAgYLL26Gss,10548
30
+ queryview/drivers/__init__.py,sha256=msOh2EgJtGE-Qus1sCLbtin_cXwmNgCFz33o_C0cU3E,359
31
+ queryview/drivers/base.py,sha256=KlL3XIqvbGB_GnDWvuRuMpdXqs0_JUpKuPNhFOEJrQ0,6626
32
+ queryview/drivers/clickhouse.py,sha256=Lw71Ujvl5znVFzLNHlB5-N6f1k4OanwpPZPj4cxNn-w,4938
33
+ queryview/drivers/duckdb.py,sha256=wgGamLKSlwN_egR_3osNgwk6JhKhv2T9BHoY0L3pMZU,5641
34
+ queryview/drivers/postgres.py,sha256=NSy1_eXAR1ui0BMd2P8b1buUKo07jK2MrrI1qFZnoDI,6044
35
+ queryview/drivers/test_base.py,sha256=VfABajT8ej4BDtv3OAoMPuPXCkdTrLAeDCvddni7Ud0,1808
36
+ queryview/drivers/test_clickhouse.py,sha256=PqR5J275VTWRWXRmwyL1NVaFAs6o5t7HZ_0FpWA1oIY,1963
37
+ queryview/drivers/test_contract.py,sha256=m1i6jKt7_pRiCASKA7TgNcrNgImZBMOFRlDwx0ebmJE,2671
38
+ queryview/drivers/test_duckdb.py,sha256=Zh4Grr8lo6QYRxOzL73T8lZXp4RjjGzPpBFwfTJGloU,2565
39
+ queryview/drivers/test_postgres.py,sha256=lnjKqbTdZq0qkZuMDLR_dCHAn2ucekNhag_Ya-YFvSQ,2534
40
+ queryview/migrations/env.py,sha256=BLN0lYLwBT-Gl5nAGQSmlPWdEQ-NQubC8Ap6EXJuuGw,1419
41
+ queryview/migrations/script.py.mako,sha256=I8FVdur13rxBM8AzY-PHgpIpwtuXxbgquD6QHTzSeQI,687
42
+ queryview/migrations/versions/9a536b7c0328_initial_schema.py,sha256=UoF755Xi7usJ1ic8TArFA1FDqVE-1AnQcLC-201_2pk,3888
43
+ queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py,sha256=cqulYLtwC0l3ePGELD13IRJ9IizmfQThcOPv06bGvO8,2393
44
+ queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py,sha256=oIbxmPwoied62M5pPuGCszt2Ou_bn2vyyM3pNVzM9xE,942
45
+ queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py,sha256=XfO6F0ZwbKTT_JUYrAEA33OS31ohFVT2MCufAzbSZN8,3897
46
+ queryview/static/favicon.svg,sha256=zurDhDS-ejtND2i4zYqiua54wmDWNDCHxuCV-AMc5P8,9523
47
+ queryview/static/index.html,sha256=K1ErE6hqmLl6-5Ldg7bigRLI3_ejwSWrvniXPhreNsc,459
48
+ queryview/static/assets/index-CvnC_D68.js,sha256=D7tm3lpnGYyfnGus3dosTWUpdhb9QRg096HPyKCGlbE,328887
49
+ queryview/static/assets/index-Qe7bhycG.css,sha256=AGivYysIrMwIfCTxDIHFi2Okz6fzHkFf7-ErAzlZLF0,27807
50
+ queryview-0.0.2.dist-info/METADATA,sha256=8NHQblmPmPjkCTHqKJyTdXDkrAxbxFaxIWniQpa-89E,6420
51
+ queryview-0.0.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
52
+ queryview-0.0.2.dist-info/entry_points.txt,sha256=5Eu59D91AZfkEb76UvcXWk2Grk17Ej-QTIvl-IkGG1s,88
53
+ queryview-0.0.2.dist-info/licenses/LICENSE,sha256=viH3V6L-xOvOuiFosKdQNQ8Xnt6bK0dnYuxhCHmQZu8,1070
54
+ queryview-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ queryview = queryview.main:run
3
+ queryview-backend = queryview.main:run
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mark kolodkin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.