Render Jinja-templated SQL files and execute them on Snowflake
Project description
snow-ops
Render Jinja-templated SQL files and execute them against Snowflake.
SQL files live in scripts/. Reusable macros live in modules/ (optional). Snowflake credentials come from environment variables or a .env file.
Installation
pip install snow-ops
Requirements: Python 3.10+
Install from source
git clone https://github.com/dbakenconsultancy/snow-ops
cd snow-ops
pip install -e .
Quick start
# Copy and fill in credentials
cp .env.example .env
# Preview rendered SQL without connecting
snow-ops --dry-run
# Run all scripts against Snowflake
snow-ops
# Run a specific script
snow-ops my_query.sql
Project layout
my_project/
├── .env # Snowflake credentials (git-ignored)
├── scripts/ # SQL files to execute
│ ├── 01_setup/
│ │ └── create_tables.sql
│ ├── 02_load/
│ │ └── load_events.sql
│ └── my_query.sql
└── modules/ # Jinja macros (optional)
└── macros.sql
scripts/ is the only required folder. modules/ is optional and can contain zero or more .sql files with reusable macros.
Files are discovered recursively across all subdirectories of scripts/ and executed in alphabetical order of their full path. Naming subdirectories with a numeric prefix (e.g. 01_setup/, 02_load/) is a simple way to control execution order.
Run snow-ops from your project root, or point to it with --project-dir.
CLI reference
snow-ops [--dry-run] [--project-dir DIR] [--var KEY=VALUE ...] [SCRIPT ...]
| Flag | Description |
|---|---|
SCRIPT ... |
One or more filenames to run (relative to scripts/, .sql extension optional). Default: all *.sql files in scripts/ and its subdirectories, sorted alphabetically by full path. |
--dry-run |
Render templates and print the resulting SQL. No Snowflake connection is made. |
--connection NAME |
Named connection from connections.toml. Overrides SNOWFLAKE_CONNECTION_NAME and individual SNOWFLAKE_* variables. |
--project-dir DIR |
Project root to use instead of the current directory. |
--var KEY=VALUE |
Pass a template variable. Repeatable. |
--version |
Print the installed version and exit. |
Examples
# Render all scripts and print them
snow-ops --dry-run
# Run two specific scripts
snow-ops load_dim.sql load_fact.sql
# Pass template variables
snow-ops --var env=prod --var run_date=2024-06-01
# Run from a different project directory
snow-ops --project-dir /path/to/project --dry-run
Jinja templating
Every .sql file in scripts/ is rendered as a Jinja template before execution. This means you can use variables, conditionals, loops, and macros.
Template variables
Pass variables at runtime with --var:
SELECT *
FROM {{ target_schema }}.orders
WHERE order_date >= '{{ run_date }}'
snow-ops --var target_schema=ANALYTICS --var run_date=2024-01-01
Use | default(...) to make a variable optional:
FROM {{ schema | default('PUBLIC') }}.orders
Environment variables
Use env_var() to read from the OS environment or .env file. This keeps runtime secrets and config separate from script-level parameters:
-- Required — fails with a clear error if not set
FROM {{ env_var('TARGET_DATABASE') }}.{{ env_var('TARGET_SCHEMA') }}.orders
-- Optional — falls back to a default
FROM {{ env_var('TARGET_SCHEMA', 'PUBLIC') }}.orders
env_var() reads from environment variables loaded at startup (including your .env file). It raises a descriptive error if the variable is missing and no default is given.
Importing macros
Place reusable macros in any .sql file inside modules/ and import them at the top of your script:
{% from 'modules/macros.sql' import ref, surrogate_key %}
SELECT
{{ surrogate_key(['user_id', 'order_date']) }} AS order_sk,
user_id,
order_date
FROM {{ ref('raw_orders', schema='RAW') }}
Built-in macros
The included modules/macros.sql provides a set of ready-to-use macros. Import only what you need.
ref(table_name, schema=none, database=none)
Generates a qualified table reference. Useful for swapping schemas between environments.
{{ ref('orders') }} -- orders
{{ ref('orders', schema='RAW') }} -- RAW.orders
{{ ref('orders', schema='RAW', database='PROD') }} -- PROD.RAW.orders
Raises a template error if database is given without schema.
surrogate_key(fields)
Generates a deterministic MD5 surrogate key from a list of column names. Nulls are coerced to empty string before hashing.
{{ surrogate_key(['user_id', 'event_date', 'event_type']) }}
Renders as:
MD5(
CONCAT_WS('|',
COALESCE(CAST(user_id AS STRING), ''),
COALESCE(CAST(event_date AS STRING), ''),
COALESCE(CAST(event_type AS STRING), '')
)
)
date_filter(col, start, end=none)
Renders a date range predicate. The end date is exclusive.
WHERE {{ date_filter('order_date', '2024-01-01') }}
-- order_date >= '2024-01-01'
WHERE {{ date_filter('order_date', '2024-01-01', '2025-01-01') }}
-- order_date >= '2024-01-01' AND order_date < '2025-01-01'
limit_rows(n=100)
Appends a LIMIT clause. Handy during development to avoid full-table scans.
{{ limit_rows() }} -- LIMIT 100
{{ limit_rows(1000) }} -- LIMIT 1000
column_list(columns)
Renders a comma-separated column list from a sequence.
SELECT {{ column_list(['user_id', 'email', 'created_at']) }}
-- user_id,
-- email,
-- created_at
Snowflake credentials
Two ways to authenticate — pick one.
Option A — connections.toml (recommended)
The Snowflake connector reads named connections from ~/.snowflake/connections.toml:
[my_connection]
account = "xy12345.us-east-1"
user = "alice"
password = "secret"
database = "ANALYTICS"
schema = "PUBLIC"
warehouse = "COMPUTE_WH"
role = "TRANSFORMER"
Pass the connection name at runtime:
snow-ops --connection my_connection
Or set it once in .env so you never have to type it:
SNOWFLAKE_CONNECTION_NAME=my_connection
--connection takes priority over SNOWFLAKE_CONNECTION_NAME, which takes priority over individual variables.
Option B — environment variables
Copy .env.example to .env and fill in your values — it is loaded automatically from the project directory.
cp .env.example .env
Required
| Variable | Description |
|---|---|
SNOWFLAKE_ACCOUNT |
Account identifier (e.g. xy12345.us-east-1) |
SNOWFLAKE_USER |
Username |
SNOWFLAKE_PASSWORD |
Password |
Optional
| Variable | Description |
|---|---|
SNOWFLAKE_DATABASE |
Default database |
SNOWFLAKE_SCHEMA |
Default schema |
SNOWFLAKE_WAREHOUSE |
Compute warehouse |
SNOWFLAKE_ROLE |
Role to assume |
Credentials are only needed for live execution. --dry-run works without them.
Writing your own macros
Add a .sql file to modules/ and define macros using standard Jinja2 syntax:
{%- macro my_macro(arg1, arg2='default') -%}
-- your SQL fragment here
{{ arg1 }} = '{{ arg2 }}'
{%- endmacro -%}
Import it in any script:
{% from 'modules/my_macros.sql' import my_macro %}
SELECT *
FROM orders
WHERE {{ my_macro('status', 'shipped') }}
The modules/ folder is optional. Scripts that do not import from it work without it.
Releasing (maintainers)
Releases are published to PyPI automatically via GitHub Actions when a GitHub Release is created.
One-time setup
Before the first release, configure a PyPI Trusted Publisher so no API token needs to be stored as a secret:
- Log in to PyPI.org and navigate to the
snow-opsproject (create it first with a manual upload if it does not exist yet). - Go to Manage → Publishing and add a new trusted publisher:
- Owner:
dbakenconsultancy - Repository:
snow-ops - Workflow filename:
publish.yml - Environment name:
pypi
- Owner:
- In the GitHub repository, create an environment named
pypi(Settings → Environments) and optionally add required reviewers for extra protection.
Cutting a release
# 1. Bump the version in pyproject.toml
# e.g. version = "0.2.0"
# 2. Commit and push
git add pyproject.toml
git commit -m "chore: bump version to 0.2.0"
git push origin main
# 3. Create and push a tag
git tag v0.2.0
git push origin v0.2.0
# 4. On GitHub: Releases → Draft a new release
# - Choose the tag you just pushed
# - Write release notes
# - Click "Publish release"
Publishing the GitHub Release triggers the publish.yml workflow, which builds the wheel and sdist, then uploads them to PyPI via OIDC (no token required).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file snow_ops-0.1.0.tar.gz.
File metadata
- Download URL: snow_ops-0.1.0.tar.gz
- Upload date:
- Size: 17.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d15b5141ca5b9a783eddf17ffb5685c6d3919ecc2bc076bdd324877c53855ec
|
|
| MD5 |
bb7ff8168dd57c0dacac96635e5d8717
|
|
| BLAKE2b-256 |
95eeffeba9f2abb4e8aeda419b0b7dbab2dfb531c12f4c13d96d736a51265821
|
Provenance
The following attestation bundles were made for snow_ops-0.1.0.tar.gz:
Publisher:
publish.yml on dbakenconsultancy/snow-ops
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snow_ops-0.1.0.tar.gz -
Subject digest:
2d15b5141ca5b9a783eddf17ffb5685c6d3919ecc2bc076bdd324877c53855ec - Sigstore transparency entry: 1642880028
- Sigstore integration time:
-
Permalink:
dbakenconsultancy/snow-ops@85def635a77bbd6e288deafbfc7d742556e7146a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/dbakenconsultancy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@85def635a77bbd6e288deafbfc7d742556e7146a -
Trigger Event:
release
-
Statement type:
File details
Details for the file snow_ops-0.1.0-py3-none-any.whl.
File metadata
- Download URL: snow_ops-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7e998093e38a828b009e8ff914fe805f8074b5a05801aa89dbcd95d5b3777767
|
|
| MD5 |
8bfc697f24b3132deac4f83cdc890289
|
|
| BLAKE2b-256 |
24078d204e6b0a9e7906a19719ba4aa6dd200c4432b78db1ea4aca94d3dcfe88
|
Provenance
The following attestation bundles were made for snow_ops-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on dbakenconsultancy/snow-ops
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snow_ops-0.1.0-py3-none-any.whl -
Subject digest:
7e998093e38a828b009e8ff914fe805f8074b5a05801aa89dbcd95d5b3777767 - Sigstore transparency entry: 1642880335
- Sigstore integration time:
-
Permalink:
dbakenconsultancy/snow-ops@85def635a77bbd6e288deafbfc7d742556e7146a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/dbakenconsultancy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@85def635a77bbd6e288deafbfc7d742556e7146a -
Trigger Event:
release
-
Statement type: