Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/py-live-warehouse.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: "Py: Live warehouse tests"

on:
push:
branches: [main]
paths: [pkg-py/**, tests/shared/**, .github/workflows/py-live-warehouse.yaml]
workflow_dispatch:

permissions:
contents: read

jobs:
live:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
backend: [databricks, snowflake]
# Each backend's credentials live in its own environment. The Databricks
# federation policy trusts this repository's `databricks-ci` environment
# by name.
environment:
name: ${{ matrix.backend }}-ci
# No deployment record for a test run. Databricks keeps the default
# until its OIDC federation is verified without one.
deployment: ${{ matrix.backend == 'databricks' }}
permissions:
contents: read
# GitHub OIDC for the Databricks service principal.
id-token: write
defaults:
run:
working-directory: pkg-py
env:
UV_PYTHON: "3.13"
DATABRICKS_HOST: ${{ vars.DATABRICKS_HOST }}
DATABRICKS_HTTP_PATH: ${{ vars.DATABRICKS_HTTP_PATH }}
DATABRICKS_CLIENT_ID: ${{ vars.DATABRICKS_CLIENT_ID }}
# Unset unless the federation policy accepts an audience other than the
# workspace's token endpoint, which is what the SDK asks GitHub for.
DATABRICKS_TOKEN_AUDIENCE: ${{ vars.DATABRICKS_TOKEN_AUDIENCE }}
# Unset when the service principal signs in with GitHub OIDC instead.
DATABRICKS_CLIENT_SECRET: ${{ secrets.DATABRICKS_CLIENT_SECRET }}
COMMONS_TEST_DATABRICKS_SCHEMA: ${{ vars.COMMONS_TEST_DATABRICKS_SCHEMA }}
COMMONS_TEST_DATABRICKS_DENIED_TABLE: ${{ vars.COMMONS_TEST_DATABRICKS_DENIED_TABLE }}
SNOWFLAKE_ACCOUNT: ${{ vars.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_USER: ${{ vars.SNOWFLAKE_USER }}
SNOWFLAKE_WAREHOUSE: ${{ vars.SNOWFLAKE_WAREHOUSE }}
COMMONS_TEST_SNOWFLAKE_SCHEMA: ${{ vars.COMMONS_TEST_SNOWFLAKE_SCHEMA }}
COMMONS_TEST_SNOWFLAKE_DENIED_TABLE: ${{ vars.COMMONS_TEST_SNOWFLAKE_DENIED_TABLE }}
COMMONS_TEST_SNOWFLAKE_ALTERNATE_ROLE: ${{ vars.COMMONS_TEST_SNOWFLAKE_ALTERNATE_ROLE }}
steps:
- uses: actions/checkout@v6
- name: Write the Snowflake private key
if: matrix.backend == 'snowflake'
env:
SNOWFLAKE_PRIVATE_KEY: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}
run: |
if [ -n "$SNOWFLAKE_PRIVATE_KEY" ]; then
key="$RUNNER_TEMP/snowflake_key.p8"
(umask 077 && printf '%s\n' "$SNOWFLAKE_PRIVATE_KEY" > "$key")
echo "SNOWFLAKE_PRIVATE_KEY_FILE=$key" >> "$GITHUB_ENV"
fi
- name: Check the configuration
# The tests skip when a required variable is unset, so a job with a
# missing variable would pass having tested nothing. Stop here instead:
# skip the job when nothing is configured, and fail it when only some
# of it is.
id: configured
run: |
backend='${{ matrix.backend }}'
names=$(jq -r --arg b "$backend" \
'.backends[$b] | .connection.required[], .fixtures.schema.variable' \
../tests/shared/live-warehouse-env.json)
missing=()
for name in $names; do
[ -n "${!name:-}" ] || missing+=("$name")
done
total=$(echo "$names" | wc -w)
if [ "${#missing[@]}" -eq "$total" ]; then
echo "::notice::$backend is not configured for this repository; skipping."
echo "run=false" >> "$GITHUB_OUTPUT"
elif [ "${#missing[@]}" -gt 0 ]; then
echo "::error::$backend is partly configured. Missing: ${missing[*]}"
exit 1
else
echo "run=true" >> "$GITHUB_OUTPUT"
fi
- uses: astral-sh/setup-uv@v6
if: steps.configured.outputs.run == 'true'
with:
enable-cache: true
cache-dependency-glob: pkg-py/uv.lock
- name: Install dependencies
if: steps.configured.outputs.run == 'true'
run: uv sync --locked --all-extras
- name: Test
if: steps.configured.outputs.run == 'true'
run: uv run pytest tests/test_live_warehouses.py -k '${{ matrix.backend }}' -rs
18 changes: 17 additions & 1 deletion pkg-py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,23 @@ shiny = [
]

[dependency-groups]
dev = ["pytest>=8", "pytest-asyncio", "ruff", "pyrefly", "pandas>=2", "polars>=1", "pins>=0.9", "commons[shiny]", "matplotlib>=3.8", "great-tables>=1.0.0", "plotnine>=0.15.8"]
dev = [
"pytest>=8",
"pytest-asyncio",
"ruff",
"pyrefly",
"pandas>=2",
"polars>=1",
"pins>=0.9",
"commons[shiny]",
"matplotlib>=3.8",
"great-tables>=1.0.0",
"plotnine>=0.15.8",
"pytz",
"databricks-sdk>=0.55",
"databricks-sqlalchemy>=2",
"snowflake-sqlalchemy>=1.6"
]
docs = [
"great-docs>=0.17",
]
Expand Down
43 changes: 43 additions & 0 deletions pkg-py/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Running the tests

`uv run pytest` runs everything. Nothing here needs credentials: a test that cannot reach what it needs skips.

## Live warehouse tests

`test_live_warehouses.py` runs against a real Snowflake or Databricks and skips without one. Every other warehouse test drives a fake replaying canned rows, which cannot tell us what a warehouse actually returns. Setting up for the live tests takes three steps.

### 1. Install the drivers

The drivers are in the `dev` group, so `uv sync` installs them.

### 2. Export the connection variables

The variables are the vendor's own, so the same export block works for the vendor CLIs and the R suite:

```
export DATABRICKS_HOST=https://example.cloud.databricks.com
export DATABRICKS_HTTP_PATH=/sql/1.0/warehouses/abc123
export COMMONS_TEST_DATABRICKS_SCHEMA=main.commons_live

export SNOWFLAKE_ACCOUNT=myorg-myaccount
export SNOWFLAKE_USER=MY_USER
export SNOWFLAKE_WAREHOUSE=MY_WH
export SNOWFLAKE_PRIVATE_KEY_FILE=~/.snowflake/keys/rsa_key.p8
export COMMONS_TEST_SNOWFLAKE_SCHEMA=COMMONS_CI.PUBLIC
```

Databricks credentials come from the SDK's [unified authentication](https://docs.databricks.com/aws/en/dev-tools/auth/unified-auth): `DATABRICKS_TOKEN`, a service principal's `DATABRICKS_CLIENT_ID` and `DATABRICKS_CLIENT_SECRET`, or a `databricks auth login` session. Snowflake signs in with a key pair registered to `SNOWFLAKE_USER`. `tests/shared/live-warehouse-env.json` is the full list of variables, including the optional ones that gate individual tests.

### 3. Create the fixture tables

```
python3 ../scripts/provision-live-warehouse.py databricks main.commons_live
```

This command prints the SQL needed to initialize the live warehouse for the test suite. It does not connect to the warehouse or run anything directly. You will need to paste the output into a Databricks SQL editor or Snowflake worksheet and run it there. The rows come from `tests/shared/live-warehouse-rows.json`, and the same file is loaded into DuckDB so the two engines' answers can be compared.

The access-refusal test needs a table the test principal cannot read, so this step involves two principals. The test principal is the one behind the connection variables from step 2: on Snowflake, the role the `SNOWFLAKE_USER` connects with; on Databricks, the application ID of the service principal the credentials resolve to (`DATABRICKS_CLIENT_ID`, when the credential is a service principal). Pass that principal to `--grant-to`, and the SQL grants it the fixture tables plus a copy of one it gets no grant on.

The second principal is the one that runs the SQL, and it owns the copy. On Databricks, that is the identity you are signed in as in the SQL editor. On Snowflake, it is the worksheet's active role, so run the SQL under a role other than the one passed to `--grant-to`, even when you sign in as a different user. An owner can always read its own tables, so if the test principal owns the copy, nothing is refused. The output ends by naming the variable to export with the copy's identifier (`COMMONS_TEST_DATABRICKS_DENIED_TABLE` or `COMMONS_TEST_SNOWFLAKE_DENIED_TABLE`), and the access-refusal test skips until it is set. Without `--grant-to` the SQL creates no denied table at all.

With those done, `uv run pytest` runs the live tests along with everything else. `test_live_warehouse_fixture.py` covers the half of this that needs no warehouse: the rows load, every corpus definition runs against them, and the provisioning SQL renders. It runs in CI with the rest of the suite. `.github/workflows/py-live-warehouse.yaml` runs the live tests themselves, with credentials from the repository's `databricks-ci` and `snowflake-ci` environments, and skips a backend whose environment is not configured.
Loading
Loading