queryview 0.0.4__tar.gz → 0.0.6__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.
- {queryview-0.0.4 → queryview-0.0.6}/.github/actions/start-git-daemon/action.yaml +3 -3
- {queryview-0.0.4 → queryview-0.0.6}/.github/workflows/publish.yaml +4 -17
- {queryview-0.0.4 → queryview-0.0.6}/.gitignore +1 -0
- {queryview-0.0.4 → queryview-0.0.6}/CLAUDE.md +12 -0
- {queryview-0.0.4 → queryview-0.0.6}/PKG-INFO +68 -12
- {queryview-0.0.4 → queryview-0.0.6}/README.md +67 -10
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/conftest.py +16 -12
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/connect.py +16 -15
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/gitsync.py +85 -23
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/env.py +1 -1
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +7 -13
- queryview-0.0.6/backend/queryview/static/assets/index-Ba8F6rEq.css +2 -0
- queryview-0.0.4/backend/queryview/static/assets/index-BE8_u4Np.js → queryview-0.0.6/backend/queryview/static/assets/index-DX2c3Adh.js +1 -1
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/static/index.html +2 -2
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_gitsync.py +1 -1
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_connect_store.py +2 -3
- queryview-0.0.6/backend/queryview/test_data_dir.py +51 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_gitsync.py +109 -9
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_mcp_gitsync.py +2 -4
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_migrations.py +5 -7
- {queryview-0.0.4 → queryview-0.0.6}/docker/Dockerfile +13 -6
- {queryview-0.0.4 → queryview-0.0.6}/docs/connect.md +10 -5
- {queryview-0.0.4 → queryview-0.0.6}/docs/future.md +3 -2
- {queryview-0.0.4 → queryview-0.0.6}/docs/gitsync.md +19 -9
- {queryview-0.0.4 → queryview-0.0.6}/docs/workspace.md +3 -4
- {queryview-0.0.4 → queryview-0.0.6}/e2e/conftest.py +11 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_app.py +11 -1
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_gitsync.py +5 -4
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/QueryView.tsx +38 -2
- queryview-0.0.6/frontend/src/app/databaseFilter.test.ts +28 -0
- queryview-0.0.6/frontend/src/app/databaseFilter.ts +7 -0
- {queryview-0.0.4 → queryview-0.0.6}/pyproject.toml +0 -1
- {queryview-0.0.4 → queryview-0.0.6}/scripts/setup_browser.sh +1 -1
- {queryview-0.0.4 → queryview-0.0.6}/uv.lock +0 -2
- queryview-0.0.4/backend/queryview/static/assets/index-B3W6A5bg.css +0 -2
- queryview-0.0.4/backend/queryview/test_db_path.py +0 -58
- {queryview-0.0.4 → queryview-0.0.6}/.claude/settings.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/.github/workflows/ci.yml +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/.mcp.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/.pre-commit-config.yaml +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/LICENSE +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/alembic.ini +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/__init__.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/dashboard_queries.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/dashboards.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/__init__.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/base.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/clickhouse.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/duckdb.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/postgres.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_base.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_clickhouse.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_contract.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_duckdb.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/drivers/test_postgres.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/main.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/mcp_server.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/script.py.mako +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/9a536b7c0328_initial_schema.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/queries.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/remote.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/static/favicon.svg +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_db.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_export_import.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_api_workspaces.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_connect_flow.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_dashboard_queries.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_dashboards.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_main.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_mcp_mount.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_queries.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_remote.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_validation.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_workspaces.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/test_yamlio.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/validation.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/workspaces.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/backend/queryview/yamlio.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/docs/api.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/docs/dashboard.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/docs/explorer.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/docs/export-import.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/docs/query.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/docs/queryview.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/docs/remote.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_dashboard.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_drivers.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_explorer.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_export_import.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_query.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_remote.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/e2e/test_workspaces.py +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/.gitignore +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/README.md +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/eslint.config.js +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/index.html +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/package.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/public/favicon.svg +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/App.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/DashboardView.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/ExplorerView.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/compactNumber.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/compactNumber.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/connection.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/ExportImportControls.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/GitSyncControls.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/Toast.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/controls/WorkspaceSwitcher.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/drivers.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/gitsync.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/gitsync.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/promptSuggestions.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/promptSuggestions.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/sessionLock.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/sessionLock.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/workspace.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/workspace.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/yamlio.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/app/yamlio.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/CellViewModal.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/ComplexCell.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellView.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellView.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellViewYaml.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/cellViewYaml.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/complexCells.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/cells/complexCells.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/dashboard/DashboardFrame.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/dashboard/srcDoc.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/dashboard/srcDoc.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/index.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/params/queryParams.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/params/queryParams.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/presentation/FieldPickers.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/presentation/presentation.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/presentation/presentation.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/results/ResultsTable.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/results/rows.test.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/core/results/rows.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/index.css +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/src/main.tsx +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/tsconfig.app.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/tsconfig.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/tsconfig.node.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/frontend/vite.config.ts +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/package-lock.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/package.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/pyrightconfig.json +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/scripts/dev.sh +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/scripts/setup.sh +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/scripts/setup_clickhouse.sh +0 -0
- {queryview-0.0.4 → queryview-0.0.6}/scripts/setup_postgres.sh +0 -0
|
@@ -3,8 +3,8 @@ description: >-
|
|
|
3
3
|
Start a loopback `git daemon` that serves — and accepts pushes into — bare
|
|
4
4
|
repos under a base path, so e2e tests exercise git sync against a real
|
|
5
5
|
git:// remote with no auth. Creates an empty bare repo and exports
|
|
6
|
-
|
|
7
|
-
steps.
|
|
6
|
+
E2E_GIT_REMOTE pointing at it, which the e2e suite applies to the default
|
|
7
|
+
workspace; `--detach` keeps the daemon alive across steps.
|
|
8
8
|
|
|
9
9
|
runs:
|
|
10
10
|
using: composite
|
|
@@ -15,7 +15,7 @@ runs:
|
|
|
15
15
|
GIT_DAEMON_BASE="$RUNNER_TEMP/gitsrv"
|
|
16
16
|
mkdir -p "$GIT_DAEMON_BASE"
|
|
17
17
|
git init --bare -b main "$GIT_DAEMON_BASE/queryview.git"
|
|
18
|
-
echo "
|
|
18
|
+
echo "E2E_GIT_REMOTE=git://127.0.0.1:9418/queryview.git" >> "$GITHUB_ENV"
|
|
19
19
|
git daemon --reuseaddr --listen=127.0.0.1 --port=9418 \
|
|
20
20
|
--base-path="$GIT_DAEMON_BASE" --export-all \
|
|
21
21
|
--enable=receive-pack --detach
|
|
@@ -221,7 +221,7 @@ jobs:
|
|
|
221
221
|
- name: Smoke-test installed package
|
|
222
222
|
working-directory: ${{ runner.temp }}
|
|
223
223
|
run: |
|
|
224
|
-
|
|
224
|
+
DATA_DIR="$PWD/qv-data" "$GITHUB_WORKSPACE/.venv/bin/queryview" > server.log 2>&1 &
|
|
225
225
|
pid=$!
|
|
226
226
|
for i in $(seq 1 30); do
|
|
227
227
|
curl -sf http://localhost:8000/api/health > /dev/null && break
|
|
@@ -417,6 +417,9 @@ jobs:
|
|
|
417
417
|
|
|
418
418
|
# Retag the exact multi-arch manifest the smoke gate tested — no
|
|
419
419
|
# rebuild, no wait-for-PyPI. Tested digests ARE the published digests.
|
|
420
|
+
# The rc tags are left in place: a GHCR package version is a digest,
|
|
421
|
+
# and vX.Y.Z / latest share the rc manifest's digest, so deleting the
|
|
422
|
+
# rc version would delete the release with it.
|
|
420
423
|
- name: Promote rc manifest
|
|
421
424
|
run: |
|
|
422
425
|
REF="ghcr.io/kolodkin/queryview"
|
|
@@ -425,19 +428,3 @@ jobs:
|
|
|
425
428
|
TAGS+=(-t "${REF}:latest")
|
|
426
429
|
fi
|
|
427
430
|
docker buildx imagetools create "${TAGS[@]}" "${REF}:${{ inputs.tag }}-rc"
|
|
428
|
-
|
|
429
|
-
# Best-effort: GITHUB_TOKEN may lack package-version delete rights;
|
|
430
|
-
# leftover -rc tags are harmless and useful for debugging.
|
|
431
|
-
- name: Clean up rc tags
|
|
432
|
-
continue-on-error: true
|
|
433
|
-
env:
|
|
434
|
-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
435
|
-
run: |
|
|
436
|
-
for suffix in rc rc-amd64 rc-arm64; do
|
|
437
|
-
TAG="${{ inputs.tag }}-${suffix}"
|
|
438
|
-
VERSION_ID=$(gh api "/user/packages/container/queryview/versions" --paginate \
|
|
439
|
-
--jq ".[] | select(.metadata.container.tags[]? == \"${TAG}\") | .id" | head -n1)
|
|
440
|
-
if [ -n "$VERSION_ID" ]; then
|
|
441
|
-
gh api -X DELETE "/user/packages/container/queryview/versions/${VERSION_ID}" || true
|
|
442
|
-
fi
|
|
443
|
-
done
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
1. Avoid `Co-Authored-By: Claude` (and similar Claude/AI attribution) trailers in
|
|
4
4
|
commit messages and PR bodies.
|
|
5
5
|
|
|
6
|
+
# Data Privacy
|
|
7
|
+
|
|
8
|
+
1. **Never commit tenant data.** Real customer, tenant, or environment
|
|
9
|
+
identifiers (database and table names, hostnames, account or user names,
|
|
10
|
+
query results, screenshots of live data) must not appear in code, tests,
|
|
11
|
+
fixtures, docs, or commit messages.
|
|
12
|
+
2. **Always anonymize.** When an example needs realistic-looking data, invent
|
|
13
|
+
generic names (e.g. `sales_reporting`, `example.internal`). Rewrite any
|
|
14
|
+
pasted samples before using them.
|
|
15
|
+
3. If tenant data slips into history, rewrite the commits to remove it and
|
|
16
|
+
force-push the branch rather than adding a follow-up commit.
|
|
17
|
+
|
|
6
18
|
# Operational Guidelines
|
|
7
19
|
|
|
8
20
|
1. Use `uv run` (e.g. `uv run pytest`, `uv run python ...`), not `python -m` or a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: queryview
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: QueryView: FastAPI + SQLModel backend and React SPA over the ClickHouse HTTP interface
|
|
5
5
|
Project-URL: Homepage, https://github.com/kolodkin/queryview
|
|
6
6
|
Project-URL: Repository, https://github.com/kolodkin/queryview
|
|
@@ -16,7 +16,6 @@ Requires-Dist: duckdb>=1.0
|
|
|
16
16
|
Requires-Dist: fastapi>=0.110
|
|
17
17
|
Requires-Dist: httpx>=0.27
|
|
18
18
|
Requires-Dist: mcp<2,>=1.9
|
|
19
|
-
Requires-Dist: platformdirs>=4
|
|
20
19
|
Requires-Dist: pyyaml>=6
|
|
21
20
|
Requires-Dist: sqlalchemy[asyncio]>=2.0
|
|
22
21
|
Requires-Dist: sqlmodel>=0.0.16
|
|
@@ -51,9 +50,69 @@ To serve on a different host port, remap it (the container keeps listening on
|
|
|
51
50
|
expects to be reached from localhost only, so prefer binding the published port
|
|
52
51
|
to loopback: `docker run -p 127.0.0.1:8000:8000 ...`.
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
The command above keeps its state inside the container, so connections and
|
|
54
|
+
workspaces are lost when the container is removed. See
|
|
55
|
+
[Run with Docker](#run-with-docker) for a persistent setup.
|
|
56
|
+
|
|
57
|
+
## Run with Docker
|
|
58
|
+
|
|
59
|
+
The image runs as the non-root user `queryview` (UID 1000) and sets
|
|
60
|
+
`DATA_DIR=/var/lib/queryview`, so every piece of state lives directly under that
|
|
61
|
+
one directory:
|
|
62
|
+
|
|
63
|
+
| Path | Contents |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `/var/lib/queryview/db.sqlite` | SQLite store: connections, workspaces, queries, dashboards |
|
|
66
|
+
| `/var/lib/queryview/encryption.key` | Key that encrypts stored connection passwords |
|
|
67
|
+
| `/var/lib/queryview/gitsync/` | Local clones used by workspace git sync |
|
|
68
|
+
|
|
69
|
+
Mount a volume at `/var/lib/queryview` and all three persist across container
|
|
70
|
+
restarts, removals and upgrades.
|
|
71
|
+
|
|
72
|
+
### Persisting it
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
mkdir -p ~/.queryview
|
|
76
|
+
docker run -d --name queryview \
|
|
77
|
+
-p 127.0.0.1:8000:8000 \
|
|
78
|
+
-v ~/.queryview:/var/lib/queryview \
|
|
79
|
+
ghcr.io/kolodkin/queryview:latest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Create the directory first so Docker doesn't create it owned by root. The
|
|
83
|
+
container runs as UID 1000, the first user on most Linux distributions; if
|
|
84
|
+
`id -u` prints something else, add `--user "$(id -u):$(id -g)"` so the
|
|
85
|
+
container writes the files as you.
|
|
86
|
+
|
|
87
|
+
`~/.queryview` is also where a local `uvx queryview` keeps its state, so the
|
|
88
|
+
container and a local run share one database, key and set of clones with no
|
|
89
|
+
extra setup. Don't run both at once: each would serve the same SQLite file.
|
|
90
|
+
|
|
91
|
+
To let Docker own the location instead, swap the path for a named volume,
|
|
92
|
+
`-v queryview-data:/var/lib/queryview`. Docker seeds it from the image, so
|
|
93
|
+
ownership is already right and there is nothing to create up front, but the
|
|
94
|
+
state no longer lines up with a local run.
|
|
95
|
+
|
|
96
|
+
### Git sync
|
|
97
|
+
|
|
98
|
+
The image ships `git`, so workspace git sync works in the container, and its
|
|
99
|
+
clones live under `/var/lib/queryview/gitsync/`, which the mount above already
|
|
100
|
+
covers. Give the workspace an HTTPS remote carrying a scoped, expiring token —
|
|
101
|
+
[docs/gitsync.md](docs/gitsync.md) has the URL form and what becomes of the
|
|
102
|
+
credential.
|
|
103
|
+
|
|
104
|
+
SSH also works, but nothing in the container can answer a prompt: the key must
|
|
105
|
+
be passphrase-free and `known_hosts` must already list the host. Mount one
|
|
106
|
+
dedicated deploy key, not your whole `.ssh`, readable by UID 1000:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
docker run -d --name queryview \
|
|
110
|
+
-p 127.0.0.1:8000:8000 \
|
|
111
|
+
-v ~/.queryview:/var/lib/queryview \
|
|
112
|
+
-v ~/.ssh/queryview_deploy:/home/queryview/.ssh/id_ed25519:ro \
|
|
113
|
+
-v ~/.ssh/known_hosts:/home/queryview/.ssh/known_hosts:ro \
|
|
114
|
+
ghcr.io/kolodkin/queryview:latest
|
|
115
|
+
```
|
|
57
116
|
|
|
58
117
|
## Layout
|
|
59
118
|
|
|
@@ -201,10 +260,7 @@ The single-page prompt UI is described in [docs/queryview.md](docs/queryview.md)
|
|
|
201
260
|
connecting (`new <type>` / `connect <name>`), SQLite persistence, and session
|
|
202
261
|
auto-connect are specified in [docs/connect.md](docs/connect.md).
|
|
203
262
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
Alongside it the backend writes a local password-encryption key
|
|
209
|
-
(`<db>.key`, override with `DB_KEY_PATH`) and the workspace git-sync clones
|
|
210
|
-
(`<db>.gitsync/`, override with `GIT_SYNC_DIR`).
|
|
263
|
+
All state lives in one data directory, `~/.queryview` on every OS, relocated
|
|
264
|
+
with `DATA_DIR`. Inside are the SQLite store `db.sqlite`, the local
|
|
265
|
+
password-encryption key `encryption.key`, and the workspace git-sync clones
|
|
266
|
+
under `gitsync/`.
|
|
@@ -26,9 +26,69 @@ To serve on a different host port, remap it (the container keeps listening on
|
|
|
26
26
|
expects to be reached from localhost only, so prefer binding the published port
|
|
27
27
|
to loopback: `docker run -p 127.0.0.1:8000:8000 ...`.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
The command above keeps its state inside the container, so connections and
|
|
30
|
+
workspaces are lost when the container is removed. See
|
|
31
|
+
[Run with Docker](#run-with-docker) for a persistent setup.
|
|
32
|
+
|
|
33
|
+
## Run with Docker
|
|
34
|
+
|
|
35
|
+
The image runs as the non-root user `queryview` (UID 1000) and sets
|
|
36
|
+
`DATA_DIR=/var/lib/queryview`, so every piece of state lives directly under that
|
|
37
|
+
one directory:
|
|
38
|
+
|
|
39
|
+
| Path | Contents |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `/var/lib/queryview/db.sqlite` | SQLite store: connections, workspaces, queries, dashboards |
|
|
42
|
+
| `/var/lib/queryview/encryption.key` | Key that encrypts stored connection passwords |
|
|
43
|
+
| `/var/lib/queryview/gitsync/` | Local clones used by workspace git sync |
|
|
44
|
+
|
|
45
|
+
Mount a volume at `/var/lib/queryview` and all three persist across container
|
|
46
|
+
restarts, removals and upgrades.
|
|
47
|
+
|
|
48
|
+
### Persisting it
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
mkdir -p ~/.queryview
|
|
52
|
+
docker run -d --name queryview \
|
|
53
|
+
-p 127.0.0.1:8000:8000 \
|
|
54
|
+
-v ~/.queryview:/var/lib/queryview \
|
|
55
|
+
ghcr.io/kolodkin/queryview:latest
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Create the directory first so Docker doesn't create it owned by root. The
|
|
59
|
+
container runs as UID 1000, the first user on most Linux distributions; if
|
|
60
|
+
`id -u` prints something else, add `--user "$(id -u):$(id -g)"` so the
|
|
61
|
+
container writes the files as you.
|
|
62
|
+
|
|
63
|
+
`~/.queryview` is also where a local `uvx queryview` keeps its state, so the
|
|
64
|
+
container and a local run share one database, key and set of clones with no
|
|
65
|
+
extra setup. Don't run both at once: each would serve the same SQLite file.
|
|
66
|
+
|
|
67
|
+
To let Docker own the location instead, swap the path for a named volume,
|
|
68
|
+
`-v queryview-data:/var/lib/queryview`. Docker seeds it from the image, so
|
|
69
|
+
ownership is already right and there is nothing to create up front, but the
|
|
70
|
+
state no longer lines up with a local run.
|
|
71
|
+
|
|
72
|
+
### Git sync
|
|
73
|
+
|
|
74
|
+
The image ships `git`, so workspace git sync works in the container, and its
|
|
75
|
+
clones live under `/var/lib/queryview/gitsync/`, which the mount above already
|
|
76
|
+
covers. Give the workspace an HTTPS remote carrying a scoped, expiring token —
|
|
77
|
+
[docs/gitsync.md](docs/gitsync.md) has the URL form and what becomes of the
|
|
78
|
+
credential.
|
|
79
|
+
|
|
80
|
+
SSH also works, but nothing in the container can answer a prompt: the key must
|
|
81
|
+
be passphrase-free and `known_hosts` must already list the host. Mount one
|
|
82
|
+
dedicated deploy key, not your whole `.ssh`, readable by UID 1000:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
docker run -d --name queryview \
|
|
86
|
+
-p 127.0.0.1:8000:8000 \
|
|
87
|
+
-v ~/.queryview:/var/lib/queryview \
|
|
88
|
+
-v ~/.ssh/queryview_deploy:/home/queryview/.ssh/id_ed25519:ro \
|
|
89
|
+
-v ~/.ssh/known_hosts:/home/queryview/.ssh/known_hosts:ro \
|
|
90
|
+
ghcr.io/kolodkin/queryview:latest
|
|
91
|
+
```
|
|
32
92
|
|
|
33
93
|
## Layout
|
|
34
94
|
|
|
@@ -176,10 +236,7 @@ The single-page prompt UI is described in [docs/queryview.md](docs/queryview.md)
|
|
|
176
236
|
connecting (`new <type>` / `connect <name>`), SQLite persistence, and session
|
|
177
237
|
auto-connect are specified in [docs/connect.md](docs/connect.md).
|
|
178
238
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Alongside it the backend writes a local password-encryption key
|
|
184
|
-
(`<db>.key`, override with `DB_KEY_PATH`) and the workspace git-sync clones
|
|
185
|
-
(`<db>.gitsync/`, override with `GIT_SYNC_DIR`).
|
|
239
|
+
All state lives in one data directory, `~/.queryview` on every OS, relocated
|
|
240
|
+
with `DATA_DIR`. Inside are the SQLite store `db.sqlite`, the local
|
|
241
|
+
password-encryption key `encryption.key`, and the workspace git-sync clones
|
|
242
|
+
under `gitsync/`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Shared fixtures for backend (non-e2e) tests.
|
|
2
2
|
|
|
3
|
-
Redirects the
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Redirects the data directory to a per-session tempdir so tests don't touch the
|
|
4
|
+
real one, and resets the lazy module-level engine/schema state in
|
|
5
|
+
`queryview.connect` before tests run."""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
@@ -14,7 +14,18 @@ import pytest
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@pytest.fixture
|
|
17
|
-
def
|
|
17
|
+
def clone_base(tmp_path, monkeypatch) -> Path:
|
|
18
|
+
"""Redirect every workspace's sync clone into this test's tmpdir, so clones
|
|
19
|
+
never land in the session data dir and can't leak between tests."""
|
|
20
|
+
from queryview import gitsync
|
|
21
|
+
|
|
22
|
+
base = tmp_path / "clones"
|
|
23
|
+
monkeypatch.setattr(gitsync, "_clone_base", lambda: base)
|
|
24
|
+
return base
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture
|
|
28
|
+
def git_env(tmp_path, clone_base):
|
|
18
29
|
"""A local bare repo as the default workspace's git-sync remote + a fresh
|
|
19
30
|
per-workspace clone base dir. Resets the default workspace to 'no remote'
|
|
20
31
|
on teardown so unconfigured-state tests stay valid."""
|
|
@@ -28,7 +39,6 @@ def git_env(tmp_path, monkeypatch):
|
|
|
28
39
|
check=True,
|
|
29
40
|
capture_output=True,
|
|
30
41
|
)
|
|
31
|
-
monkeypatch.setenv("GIT_SYNC_DIR", str(tmp_path / "clones"))
|
|
32
42
|
asyncio.run(update_workspace(DEFAULT_WORKSPACE, remote=str(remote)))
|
|
33
43
|
yield remote
|
|
34
44
|
asyncio.run(update_workspace(DEFAULT_WORKSPACE, remote=None))
|
|
@@ -69,13 +79,7 @@ def default_ws_id() -> int:
|
|
|
69
79
|
@pytest.fixture(scope="session", autouse=True)
|
|
70
80
|
def _isolated_db(tmp_path_factory: pytest.TempPathFactory):
|
|
71
81
|
tmp: Path = tmp_path_factory.mktemp("qv_backend_tests")
|
|
72
|
-
os.environ["
|
|
73
|
-
os.environ["DB_KEY_PATH"] = str(tmp / "test.db.key")
|
|
74
|
-
|
|
75
|
-
# The workspaces migration seeds the default workspace from GIT_SYNC_*;
|
|
76
|
-
# tests control that per-test (monkeypatch), never from ambient env.
|
|
77
|
-
os.environ.pop("GIT_SYNC_REMOTE", None)
|
|
78
|
-
os.environ.pop("GIT_SYNC_BRANCH", None)
|
|
82
|
+
os.environ["DATA_DIR"] = str(tmp)
|
|
79
83
|
|
|
80
84
|
# Reset the lazy globals so the next DB touch picks up the new paths.
|
|
81
85
|
import queryview.connect as _c
|
|
@@ -14,7 +14,6 @@ from dataclasses import dataclass
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
16
16
|
|
|
17
|
-
import platformdirs
|
|
18
17
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
19
18
|
from sqlalchemy.ext.asyncio import create_async_engine
|
|
20
19
|
from sqlmodel import Field, SQLModel, col, select
|
|
@@ -40,15 +39,18 @@ class Connection(SQLModel, table=True):
|
|
|
40
39
|
last_active_at: int # unix ms; the max is the "latest active"
|
|
41
40
|
|
|
42
41
|
|
|
42
|
+
def _data_dir() -> Path:
|
|
43
|
+
"""Every piece of state lives here: DATA_DIR, else `~/.queryview` — the same
|
|
44
|
+
path on every OS, and the image's mount point, so a local run and a container
|
|
45
|
+
share state. Deliberately not package-relative: that put the DB in
|
|
46
|
+
site-packages, which is uv's disposable cache under `uvx`."""
|
|
47
|
+
env = os.environ.get("DATA_DIR")
|
|
48
|
+
return Path(env) if env else Path.home() / ".queryview"
|
|
49
|
+
|
|
50
|
+
|
|
43
51
|
def _db_path() -> Path:
|
|
44
|
-
"""The SQLite store
|
|
45
|
-
(
|
|
46
|
-
`%LOCALAPPDATA%` on Windows). Deliberately not package-relative — that put
|
|
47
|
-
the DB in site-packages, which is uv's disposable cache under `uvx`."""
|
|
48
|
-
env = os.environ.get("DB_PATH")
|
|
49
|
-
if env:
|
|
50
|
-
return Path(env)
|
|
51
|
-
return Path(platformdirs.user_data_dir("queryview")) / "queryview.db"
|
|
52
|
+
"""The SQLite store: a fixed name in the data dir — move the dir, not it."""
|
|
53
|
+
return _data_dir() / "db.sqlite"
|
|
52
54
|
|
|
53
55
|
|
|
54
56
|
_engine = None
|
|
@@ -67,7 +69,7 @@ def _engine_for_db():
|
|
|
67
69
|
def _alembic_config() -> Config:
|
|
68
70
|
"""Alembic Config built in code (not a cwd alembic.ini) so migrations run from
|
|
69
71
|
any directory and from the packaged wheel. Points at the package's migrations
|
|
70
|
-
dir and injects a *sync* SQLite URL for the current
|
|
72
|
+
dir and injects a *sync* SQLite URL for the current data dir."""
|
|
71
73
|
from alembic.config import Config
|
|
72
74
|
|
|
73
75
|
cfg = Config()
|
|
@@ -85,9 +87,9 @@ async def _ensure_schema() -> None:
|
|
|
85
87
|
return
|
|
86
88
|
from alembic import command
|
|
87
89
|
|
|
88
|
-
# SQLite won't create a missing parent, and the
|
|
89
|
-
#
|
|
90
|
-
|
|
90
|
+
# SQLite won't create a missing parent, and the data dir doesn't exist on a
|
|
91
|
+
# fresh install; the key file and git-sync clones land here too.
|
|
92
|
+
_data_dir().mkdir(parents=True, exist_ok=True)
|
|
91
93
|
command.upgrade(_alembic_config(), "head")
|
|
92
94
|
_schema_ready = True
|
|
93
95
|
|
|
@@ -99,8 +101,7 @@ async def _ensure_schema() -> None:
|
|
|
99
101
|
|
|
100
102
|
|
|
101
103
|
def _key_path() -> Path:
|
|
102
|
-
|
|
103
|
-
return Path(env) if env else Path(f"{_db_path()}.key")
|
|
104
|
+
return _data_dir() / "encryption.key"
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
_key: bytes | None = None
|
|
@@ -11,9 +11,11 @@ import os
|
|
|
11
11
|
import shutil
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any
|
|
14
|
+
from urllib.parse import unquote, urlsplit, urlunsplit
|
|
14
15
|
|
|
15
16
|
import yaml
|
|
16
17
|
|
|
18
|
+
from .connect import _data_dir
|
|
17
19
|
from .workspaces import WorkspaceRec
|
|
18
20
|
from .yamlio import YamlIOError, dashboard_from_data, dump_yaml, query_from_data, query_to_data, slug
|
|
19
21
|
|
|
@@ -86,8 +88,7 @@ def dashboard_from_files(files: dict[str, str]) -> dict[str, Any]:
|
|
|
86
88
|
|
|
87
89
|
|
|
88
90
|
# --- Configuration ---------------------------------------------------------
|
|
89
|
-
#
|
|
90
|
-
# are read once, by the workspaces migration, to seed the default workspace).
|
|
91
|
+
# All config lives on the workspace row; there is no environment fallback.
|
|
91
92
|
|
|
92
93
|
|
|
93
94
|
def _require_remote(ws: WorkspaceRec) -> str:
|
|
@@ -96,15 +97,15 @@ def _require_remote(ws: WorkspaceRec) -> str:
|
|
|
96
97
|
return ws.remote
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
def _clone_base() -> Path:
|
|
101
|
+
"""Where every workspace's sync clone lives, inside the data dir."""
|
|
102
|
+
return _data_dir() / "gitsync"
|
|
103
|
+
|
|
104
|
+
|
|
99
105
|
def _workdir(ws: WorkspaceRec) -> Path:
|
|
100
106
|
"""This workspace's clone: {base}/{workspace id}. Keyed by id so renaming
|
|
101
|
-
a workspace never orphans its clone.
|
|
102
|
-
|
|
103
|
-
if env:
|
|
104
|
-
return Path(env) / str(ws.id)
|
|
105
|
-
from .connect import _db_path
|
|
106
|
-
|
|
107
|
-
return Path(f"{_db_path()}.gitsync") / str(ws.id)
|
|
107
|
+
a workspace never orphans its clone."""
|
|
108
|
+
return _clone_base() / str(ws.id)
|
|
108
109
|
|
|
109
110
|
|
|
110
111
|
def configured(ws: WorkspaceRec) -> bool:
|
|
@@ -129,15 +130,73 @@ def _lock(ws: WorkspaceRec) -> asyncio.Lock:
|
|
|
129
130
|
return lock
|
|
130
131
|
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
)
|
|
140
|
-
|
|
133
|
+
# Seconds before a git invocation is killed; only network calls come close.
|
|
134
|
+
_GIT_TIMEOUT_S = 120
|
|
135
|
+
|
|
136
|
+
# Feeds the credential back per invocation from the child's environment, so it
|
|
137
|
+
# reaches neither the repo's config nor our argv. Git calls a helper with "get",
|
|
138
|
+
# "store" or "erase"; answering only "get" avoids writing it anywhere.
|
|
139
|
+
_CREDENTIAL_HELPER = (
|
|
140
|
+
'!f() { test "$1" = get && printf "username=%s\\npassword=%s\\n" "$QV_GIT_USERNAME" "$QV_GIT_PASSWORD"; }; f'
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _split_credential(url: str) -> tuple[str, tuple[str, str] | None]:
|
|
145
|
+
"""Split an http(s) URL's credential from the URL, so the URL can be handed
|
|
146
|
+
to git (and persisted in the clone's config) without it.
|
|
147
|
+
|
|
148
|
+
Only http(s) userinfo is a secret: `git@host:path` and `ssh://git@host` name
|
|
149
|
+
an SSH login, so those are returned untouched."""
|
|
150
|
+
parts = urlsplit(url)
|
|
151
|
+
if parts.scheme not in ("http", "https") or not (parts.username or parts.password):
|
|
152
|
+
return url, None
|
|
153
|
+
host = parts.hostname or ""
|
|
154
|
+
if parts.port:
|
|
155
|
+
host = f"{host}:{parts.port}"
|
|
156
|
+
sanitized = urlunsplit((parts.scheme, host, parts.path, parts.query, parts.fragment))
|
|
157
|
+
return sanitized, (unquote(parts.username or ""), unquote(parts.password or ""))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _credential(ws: WorkspaceRec) -> tuple[str, str] | None:
|
|
161
|
+
"""This workspace's git credential, if its remote embeds one."""
|
|
162
|
+
return _split_credential(_require_remote(ws))[1]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def _git(*args: str, cwd: Path | None = None, credential: tuple[str, str] | None = None) -> str:
|
|
166
|
+
"""Run one git command, never interactively: stdin is closed and any prompt
|
|
167
|
+
(terminal, SSH passphrase, unknown host) becomes a failure rather than a
|
|
168
|
+
blocked request. A credential, when passed, arrives via the environment."""
|
|
169
|
+
env = dict(os.environ)
|
|
170
|
+
env["GIT_TERMINAL_PROMPT"] = "0"
|
|
171
|
+
env["GIT_SSH_COMMAND"] = f"{os.environ.get('GIT_SSH_COMMAND', 'ssh')} -o BatchMode=yes"
|
|
172
|
+
if credential is not None:
|
|
173
|
+
env["QV_GIT_USERNAME"], env["QV_GIT_PASSWORD"] = credential
|
|
174
|
+
# Config through the environment, not `-c` flags, so argv stays exactly
|
|
175
|
+
# what the caller passed. The empty first value clears any helper
|
|
176
|
+
# inherited from system/global config, leaving ours the only one asked.
|
|
177
|
+
env["GIT_CONFIG_COUNT"] = "2"
|
|
178
|
+
env["GIT_CONFIG_KEY_0"], env["GIT_CONFIG_VALUE_0"] = "credential.helper", ""
|
|
179
|
+
env["GIT_CONFIG_KEY_1"], env["GIT_CONFIG_VALUE_1"] = "credential.helper", _CREDENTIAL_HELPER
|
|
180
|
+
try:
|
|
181
|
+
proc = await asyncio.create_subprocess_exec(
|
|
182
|
+
"git",
|
|
183
|
+
*args,
|
|
184
|
+
cwd=str(cwd) if cwd else None,
|
|
185
|
+
env=env,
|
|
186
|
+
stdin=asyncio.subprocess.DEVNULL,
|
|
187
|
+
stdout=asyncio.subprocess.PIPE,
|
|
188
|
+
stderr=asyncio.subprocess.PIPE,
|
|
189
|
+
)
|
|
190
|
+
except FileNotFoundError as e:
|
|
191
|
+
# No `git` on PATH: a deployment problem, not a repo problem — say so
|
|
192
|
+
# instead of surfacing a bare FileNotFoundError as a 500.
|
|
193
|
+
raise GitSyncError("git is not installed on the server") from e
|
|
194
|
+
try:
|
|
195
|
+
out, err = await asyncio.wait_for(proc.communicate(), _GIT_TIMEOUT_S)
|
|
196
|
+
except TimeoutError as e:
|
|
197
|
+
proc.kill()
|
|
198
|
+
await proc.wait()
|
|
199
|
+
raise GitSyncError(f"git {args[0]} timed out after {_GIT_TIMEOUT_S}s") from e
|
|
141
200
|
if proc.returncode != 0:
|
|
142
201
|
tail = err.decode("utf-8", "replace").strip()[-500:]
|
|
143
202
|
raise GitSyncError(f"git {args[0]} failed: {tail}")
|
|
@@ -147,19 +206,22 @@ async def _git(*args: str, cwd: Path | None = None) -> str:
|
|
|
147
206
|
async def _ensure_repo(ws: WorkspaceRec) -> Path:
|
|
148
207
|
"""The workspace's sync clone, cloning (or, for an empty remote, init +
|
|
149
208
|
remote add) on first use. Idempotent."""
|
|
150
|
-
|
|
209
|
+
branch, wd = ws.branch, _workdir(ws)
|
|
151
210
|
if (wd / ".git").exists():
|
|
152
211
|
return wd
|
|
153
212
|
wd.parent.mkdir(parents=True, exist_ok=True)
|
|
213
|
+
# The clone records its remote in .git/config, so only the credential-free
|
|
214
|
+
# URL goes to git as an argument; the credential travels out of band.
|
|
215
|
+
remote, cred = _split_credential(_require_remote(ws))
|
|
154
216
|
try:
|
|
155
|
-
await _git("clone", "--branch", branch, remote, str(wd))
|
|
217
|
+
await _git("clone", "--branch", branch, remote, str(wd), credential=cred)
|
|
156
218
|
except GitSyncError as clone_err:
|
|
157
219
|
# Clone can fail either because the branch genuinely doesn't exist yet
|
|
158
220
|
# on an empty remote (the only case we should paper over with a local
|
|
159
221
|
# init) or because the remote itself is unreachable/misconfigured. Ask
|
|
160
222
|
# the remote directly to tell the two apart.
|
|
161
223
|
try:
|
|
162
|
-
heads = await _git("ls-remote", "--heads", remote, branch)
|
|
224
|
+
heads = await _git("ls-remote", "--heads", remote, branch, credential=cred)
|
|
163
225
|
except GitSyncError as probe_err:
|
|
164
226
|
raise GitSyncError(f"git remote unreachable: {probe_err}") from probe_err
|
|
165
227
|
if heads.strip():
|
|
@@ -177,7 +239,7 @@ async def _ensure_repo(ws: WorkspaceRec) -> Path:
|
|
|
177
239
|
async def _origin_head(wd: Path, ws: WorkspaceRec) -> str | None:
|
|
178
240
|
"""Fetch, then the remote branch ref if it exists (None on an empty remote).
|
|
179
241
|
Network/auth failures raise."""
|
|
180
|
-
await _git("fetch", "origin", cwd=wd)
|
|
242
|
+
await _git("fetch", "origin", cwd=wd, credential=_credential(ws))
|
|
181
243
|
ref = f"origin/{ws.branch}"
|
|
182
244
|
try:
|
|
183
245
|
await _git("rev-parse", "--verify", ref, cwd=wd)
|
|
@@ -257,7 +319,7 @@ async def store(
|
|
|
257
319
|
return {"committed": False, "sha": None, "message": "no changes"}
|
|
258
320
|
label = f"{conn_type}/{name}" if kind == "query" else name
|
|
259
321
|
await _git("commit", "-m", message or f"store {kind} {label}", cwd=wd)
|
|
260
|
-
await _git("push", "origin", ws.branch, cwd=wd)
|
|
322
|
+
await _git("push", "origin", ws.branch, cwd=wd, credential=_credential(ws))
|
|
261
323
|
sha = (await _git("rev-parse", "HEAD", cwd=wd)).strip()
|
|
262
324
|
return {"committed": True, "sha": sha, "message": "stored"}
|
|
263
325
|
|
|
@@ -21,7 +21,7 @@ target_metadata = SQLModel.metadata
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _url() -> str:
|
|
24
|
-
"""Prefer the URL the app injects; fall back to
|
|
24
|
+
"""Prefer the URL the app injects; fall back to the data dir so the dev CLI
|
|
25
25
|
(`alembic ...`) targets the same SQLite file the app would."""
|
|
26
26
|
from queryview.connect import _db_path
|
|
27
27
|
|
{queryview-0.0.4 → queryview-0.0.6}/backend/queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""workspaces: per-workspace git sync — workspaces table, entity workspace_id
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Creates the 'default' workspace (its remote is configured through the UI/API
|
|
4
|
+
afterwards) and backfills all existing predefined queries and dashboards into
|
|
5
|
+
it. Name uniqueness becomes
|
|
6
6
|
per-workspace.
|
|
7
7
|
|
|
8
8
|
Revision ID: c7d8e9f0a1b2
|
|
@@ -13,7 +13,6 @@ Create Date: 2026-07-10
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import os
|
|
17
16
|
from collections.abc import Sequence
|
|
18
17
|
|
|
19
18
|
import sqlalchemy as sa
|
|
@@ -37,17 +36,12 @@ def upgrade() -> None:
|
|
|
37
36
|
)
|
|
38
37
|
op.create_index(op.f("ix_workspaces_name"), "workspaces", ["name"], unique=True)
|
|
39
38
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
from queryview.connect import _encrypt_str
|
|
43
|
-
|
|
44
|
-
env_remote = os.environ.get("GIT_SYNC_REMOTE")
|
|
45
|
-
remote = _encrypt_str(env_remote) if env_remote else None
|
|
46
|
-
branch = os.environ.get("GIT_SYNC_BRANCH") or "main"
|
|
39
|
+
# No remote yet: git sync stays disabled until one is set through the
|
|
40
|
+
# UI/API, which encrypts it on the way in.
|
|
47
41
|
conn = op.get_bind()
|
|
48
42
|
conn.execute(
|
|
49
|
-
sa.text("INSERT INTO workspaces (name, remote, branch) VALUES (:n,
|
|
50
|
-
{"n": "default", "
|
|
43
|
+
sa.text("INSERT INTO workspaces (name, remote, branch) VALUES (:n, NULL, :b)"),
|
|
44
|
+
{"n": "default", "b": "main"},
|
|
51
45
|
)
|
|
52
46
|
default_id = conn.execute(sa.text("SELECT id FROM workspaces WHERE name = 'default'")).scalar()
|
|
53
47
|
|