Skip to content
Merged
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
96 changes: 96 additions & 0 deletions .github/workflows/py-live-warehouse.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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: ${{ matrix.backend }}-ci
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 --group live-warehouse
- name: Test
if: steps.configured.outputs.run == 'true'
run: uv run pytest tests/test_live_warehouses.py -k '${{ matrix.backend }}' -rs
12 changes: 11 additions & 1 deletion pkg-py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,20 @@ 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"]
# pytz is what DuckDB reaches for to hand back a TIMESTAMPTZ, which the live
# warehouse fixture rows use so their offsets survive the round trip.
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"]
docs = [
"great-docs>=0.17",
]
# Drivers for the live warehouse tests, which skip without a configured
# warehouse. Kept out of `dev` so an ordinary contributor does not install a
# warehouse client to run the suite.
live-warehouse = [
"databricks-sdk>=0.55",
"databricks-sqlalchemy>=2",
"snowflake-sqlalchemy>=1.6",
]

[build-system]
requires = ["hatchling"]
Expand Down
63 changes: 63 additions & 0 deletions pkg-py/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# 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. It exists because every other warehouse test drives a fake
replaying canned rows, which cannot tell us what a warehouse actually returns.

Install the drivers, which are deliberately outside the `dev` group so an
ordinary contributor does not install a warehouse client:

```
uv sync --group live-warehouse
```

Point at a warehouse. The connection variables are the vendor's own, so one
export block also serves 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.

Create the tables the tests expect:

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

That prints SQL to run; it does not connect. 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. Pass
`--grant-to` with the principal the tests connect as (a Snowflake role, or a
Databricks service principal's application ID), and the SQL grants it the
fixture tables and creates a copy it is refused. Without `--grant-to` there is
no such table, so that test skips.

`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