streamsnow 0.1.0__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 (37) hide show
  1. streamsnow/__init__.py +19 -0
  2. streamsnow/_templates/app/AGENTS.md.j2 +15 -0
  3. streamsnow/_templates/app/branding.py.j2 +51 -0
  4. streamsnow/_templates/app/config.toml.j2 +8 -0
  5. streamsnow/_templates/app/environment.yml.j2 +11 -0
  6. streamsnow/_templates/app/example_metric.sql.j2 +12 -0
  7. streamsnow/_templates/app/overview.py.j2 +48 -0
  8. streamsnow/_templates/app/pyproject.toml.j2 +12 -0
  9. streamsnow/_templates/app/secrets.toml.example.j2 +10 -0
  10. streamsnow/_templates/app/snowflake.yml.j2 +29 -0
  11. streamsnow/_templates/app/sql_loader.py.j2 +21 -0
  12. streamsnow/_templates/app/streamlit_app.py.j2 +16 -0
  13. streamsnow/_templates/repo/AGENTS.md.j2 +52 -0
  14. streamsnow/_templates/repo/CLAUDE.md.j2 +1 -0
  15. streamsnow/_templates/repo/README.md.j2 +33 -0
  16. streamsnow/_templates/repo/ci.yml.j2 +35 -0
  17. streamsnow/_templates/repo/deploy.git.yml.j2 +66 -0
  18. streamsnow/_templates/repo/deploy.yml.j2 +68 -0
  19. streamsnow/_templates/repo/gitignore.j2 +21 -0
  20. streamsnow/_templates/repo/pre-commit-config.yaml.j2 +42 -0
  21. streamsnow/cli.py +594 -0
  22. streamsnow/config.py +401 -0
  23. streamsnow/deploy.py +152 -0
  24. streamsnow/policy.py +48 -0
  25. streamsnow/scaffolder.py +188 -0
  26. streamsnow/tools/__init__.py +8 -0
  27. streamsnow/tools/check_app_security.py +643 -0
  28. streamsnow/tools/check_bind_predicates.py +66 -0
  29. streamsnow/tools/check_caching.py +186 -0
  30. streamsnow/tools/check_export_clean.py +123 -0
  31. streamsnow/tools/check_schema_refs.py +272 -0
  32. streamsnow/tools/validate_app.py +281 -0
  33. streamsnow-0.1.0.dist-info/METADATA +149 -0
  34. streamsnow-0.1.0.dist-info/RECORD +37 -0
  35. streamsnow-0.1.0.dist-info/WHEEL +4 -0
  36. streamsnow-0.1.0.dist-info/entry_points.txt +2 -0
  37. streamsnow-0.1.0.dist-info/licenses/LICENSE +21 -0
streamsnow/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """StreamSnow — open-source toolkit for Streamlit-in-Snowflake apps with Claude Code.
2
+
3
+ The ``streamsnow`` package is the single source of truth for tool logic. The CLI,
4
+ the Claude Code plugin, pre-commit, and CI all call into this package — one
5
+ implementation, many consumers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__ = ["__version__"]
11
+
12
+ # Keep in sync with [project.version] in pyproject.toml. Read dynamically when
13
+ # installed so `streamsnow --version` reflects the resolved distribution.
14
+ try: # pragma: no cover - trivial
15
+ from importlib.metadata import version as _version
16
+
17
+ __version__ = _version("streamsnow")
18
+ except Exception: # pragma: no cover - source checkout without install
19
+ __version__ = "0.1.0"
@@ -0,0 +1,15 @@
1
+ # {{ app_title }}
2
+
3
+ App-specific context. Inherits the repo rulebook in the top-level `AGENTS.md`.
4
+
5
+ - **Runtime:** {{ runtime }} · **Query warehouse:** `{{ default_warehouse }}`
6
+ - **Data sources:** `{{ gov_database }}` (schemas: {{ schema_allow | join(', ') }})
7
+ - **Caching strategy:** default TTL {{ cache_ttl }}s. Note any per-query deviations here with a reason.
8
+
9
+ ## Pages
10
+
11
+ - **Overview** (`pages/overview.py`) — starter page; replace the example metric with your real KPIs.
12
+
13
+ ## Queries
14
+
15
+ - `queries/example_metric.sql` — placeholder; repoint `YOUR_TABLE` at a real object in an allowed schema.
@@ -0,0 +1,51 @@
1
+ """Branding for {{ project_name }} — generated by StreamSnow from brand config.
2
+
3
+ Each app ships its own local copy (deploy runtimes can't import a shared module).
4
+ Import locally: ``from branding import apply_branding, branded_metric``.
5
+ """
6
+
7
+ import plotly.graph_objects as go
8
+ import plotly.io as pio
9
+ import streamlit as st
10
+
11
+ _BRANDING_VERSION = "1.0.0"
12
+
13
+ BRAND_COLORS = {
14
+ "primary": "{{ brand.primary }}",
15
+ "background": "{{ brand.background }}",
16
+ "text": "{{ brand.text_color }}",
17
+ }
18
+ BRAND_CHART_COLORS = [{{ brand.chart_sequence | map('tojson') | join(", ") }}]
19
+
20
+ # Register the Plotly template once. Guard against re-registration AND the
21
+ # default-set side effect: the container runtime shares one Python process
22
+ # across all viewers, so module-import mutations must be idempotent.
23
+ if "streamsnow_brand" not in pio.templates:
24
+ pio.templates["streamsnow_brand"] = go.layout.Template(
25
+ layout=dict(
26
+ font=dict(family="{{ brand.font }}", color="{{ brand.text_color }}"),
27
+ colorway=BRAND_CHART_COLORS,
28
+ paper_bgcolor="{{ brand.background }}",
29
+ plot_bgcolor="{{ brand.background }}",
30
+ )
31
+ )
32
+ pio.templates.default = "streamsnow_brand"
33
+ BRAND_PLOTLY_TEMPLATE = "streamsnow_brand"
34
+
35
+
36
+ def apply_branding() -> None:
37
+ """Call once, right after ``st.set_page_config``. Sets the Plotly default."""
38
+ pio.templates.default = "streamsnow_brand"
39
+
40
+
41
+ def branded_metric(label: str, value: str, delta: str | None = None, border_color: str | None = None) -> None:
42
+ """A metric card with a brand-colored left border."""
43
+ color = border_color or BRAND_COLORS["primary"]
44
+ delta_html = f'<div style="font-size:0.8rem;color:#6b7280;">{delta}</div>' if delta else ""
45
+ st.markdown(
46
+ f'<div style="border-left:4px solid {color};padding:0.25rem 0.75rem;margin:0.25rem 0;">'
47
+ f'<div style="font-size:0.8rem;color:#6b7280;">{label}</div>'
48
+ f'<div style="font-size:1.6rem;font-weight:600;">{value}</div>'
49
+ f"{delta_html}</div>",
50
+ unsafe_allow_html=True,
51
+ )
@@ -0,0 +1,8 @@
1
+ [theme]
2
+ base = "light"
3
+ primaryColor = "{{ brand.primary }}"
4
+ backgroundColor = "{{ brand.background }}"
5
+ secondaryBackgroundColor = "{{ brand.secondary_background }}"
6
+ textColor = "{{ brand.text_color }}"
7
+ font = "sans serif"
8
+ chartCategoricalColors = [{{ brand.chart_sequence | map('tojson') | join(", ") }}]
@@ -0,0 +1,11 @@
1
+ # Warehouse-runtime dependencies (Snowflake Anaconda channel).
2
+ # LANDMINE: never pin `python` here — the channel has no exact python==3.11
3
+ # build, so the deploy fails. The runtime supplies Python; list only added deps.
4
+ name: sf_env
5
+ channels:
6
+ - snowflake
7
+ dependencies:
8
+ - streamlit=1.50.0
9
+ - pandas
10
+ - plotly
11
+ - snowflake-snowpark-python
@@ -0,0 +1,12 @@
1
+ -- Query: example_metric
2
+ -- Feeds: Overview page (example metric + trend)
3
+ -- Schemas: {{ gov_database }}.{{ schema_allow[0] }}
4
+ -- Params: :1 start_date, :2 end_date
5
+ SELECT
6
+ metric_date AS dt,
7
+ COUNT(*) AS n,
8
+ SUM(amount) AS total
9
+ FROM {{ gov_database }}.{{ schema_allow[0] }}.YOUR_TABLE -- TODO: replace YOUR_TABLE
10
+ WHERE metric_date BETWEEN :1 AND :2
11
+ GROUP BY metric_date
12
+ ORDER BY metric_date
@@ -0,0 +1,48 @@
1
+ """Overview page for {{ app_title }}. Scaffolded by StreamSnow ({{ runtime }} runtime).
2
+
3
+ Replace the example metric with your real KPIs and repoint the query at a real
4
+ table in an allowed schema ({{ schema_allow | join(', ') }}).
5
+ """
6
+
7
+ import plotly.express as px
8
+ import streamlit as st
9
+
10
+ from branding import BRAND_CHART_COLORS, branded_metric
11
+ from sql_loader import load_sql
12
+
13
+
14
+ @st.cache_data(ttl={{ cache_ttl }})
15
+ def load_example(start_date: str, end_date: str):
16
+ """Example cached loader. Wire to real filters once YOUR_TABLE is replaced."""
17
+ sql = load_sql("example_metric")
18
+ {% if runtime == 'container' %}
19
+ conn = st.connection("snowflake")
20
+ return conn.query(sql, params=[start_date, end_date], ttl=0)
21
+ {% else %}
22
+ try:
23
+ from snowflake.snowpark.context import get_active_session
24
+
25
+ session = get_active_session()
26
+ except Exception:
27
+ session = st.connection("snowflake").session()
28
+ return session.sql(sql, params=[start_date, end_date]).to_pandas()
29
+ {% endif %}
30
+
31
+
32
+ st.title("{{ app_title }}")
33
+ st.caption("Starter page scaffolded by StreamSnow. Replace the example with your data.")
34
+
35
+ st.subheader("Example metric")
36
+ st.caption("Demonstrates branded_metric and a Plotly chart using your brand colors.")
37
+ branded_metric("Rows", "1,234", delta="+5.3%")
38
+
39
+ fig = px.bar(
40
+ x=["alpha", "beta", "gamma"],
41
+ y=[3, 1, 2],
42
+ labels={"x": "category", "y": "count"},
43
+ color_discrete_sequence=BRAND_CHART_COLORS,
44
+ )
45
+ st.plotly_chart(fig, use_container_width=True)
46
+
47
+ st.divider()
48
+ st.caption("Sources: {{ gov_database }}.{{ schema_allow[0] }} (example)")
@@ -0,0 +1,12 @@
1
+ # Container-runtime dependencies (PyPI via external access integration).
2
+ # PEP 440 specifiers — NOT conda wildcards. Do not add a separate python pin here.
3
+ [project]
4
+ name = "{{ app_slug }}"
5
+ version = "0.1.0"
6
+ requires-python = ">={{ container_python }},<3.{{ container_python.split('.')[1] | int + 1 }}"
7
+ dependencies = [
8
+ "streamlit==1.50.0",
9
+ "pandas>=2,<3",
10
+ "plotly>=5,<6",
11
+ "snowflake-snowpark-python",
12
+ ]
@@ -0,0 +1,10 @@
1
+ # Local preview only. Copy to secrets.toml (gitignored) and fill in.
2
+ # secrets.toml is NEVER committed.
3
+ [connections.snowflake]
4
+ account = "{{ account }}"
5
+ user = "<your_username>"
6
+ authenticator = "externalbrowser"
7
+ role = "{{ viewer_role }}"
8
+ warehouse = "{{ default_warehouse }}"
9
+ database = "{{ gov_database }}"
10
+ schema = "{{ schema_allow[0] }}"
@@ -0,0 +1,29 @@
1
+ definition_version: 2
2
+ entities:
3
+ {{ app_slug | replace('-', '_') }}:
4
+ type: streamlit
5
+ identifier:
6
+ name: {{ app_slug | replace('-', '_') | upper }}
7
+ database: {{ app_database }}
8
+ schema: {{ app_schema }}
9
+ main_file: streamlit_app.py
10
+ query_warehouse: {{ default_warehouse }}
11
+ title: "{{ app_title }}"
12
+ {% if runtime == 'container' %}
13
+ runtime_name: {{ runtime_name }}
14
+ compute_pool: {{ compute_pool }}
15
+ external_access_integrations:
16
+ - {{ external_access_integration }}
17
+ {% endif %}
18
+ artifacts:
19
+ - streamlit_app.py
20
+ - pages/
21
+ - queries/
22
+ - branding.py
23
+ - sql_loader.py
24
+ - .streamlit/config.toml
25
+ {% if runtime == 'container' %}
26
+ - pyproject.toml
27
+ {% else %}
28
+ - environment.yml
29
+ {% endif %}
@@ -0,0 +1,21 @@
1
+ """Load SQL from ``queries/``. Each app ships its own copy.
2
+
3
+ - ``load_sql(name)`` returns the raw file contents.
4
+ - ``render_sql(name, **tokens)`` substitutes ``{UPPERCASE_TOKEN}`` placeholders
5
+ via str.replace (NOT str.format — SQL patterns like ``{2}`` would collide).
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ _QUERIES = Path(__file__).parent / "queries"
11
+
12
+
13
+ def load_sql(name: str) -> str:
14
+ return (_QUERIES / f"{name}.sql").read_text()
15
+
16
+
17
+ def render_sql(name: str, **tokens: str) -> str:
18
+ sql = load_sql(name)
19
+ for key, value in tokens.items():
20
+ sql = sql.replace("{" + key + "}", value)
21
+ return sql
@@ -0,0 +1,16 @@
1
+ """{{ app_title }} — entrypoint. Scaffolded by StreamSnow ({{ runtime }} runtime)."""
2
+
3
+ import streamlit as st
4
+
5
+ st.set_page_config(page_title="{{ app_title }}", page_icon="📊", layout="wide")
6
+
7
+ from branding import apply_branding # noqa: E402 (must follow set_page_config)
8
+
9
+ apply_branding()
10
+
11
+ nav = st.navigation(
12
+ [
13
+ st.Page("pages/overview.py", title="Overview", icon="📈", default=True),
14
+ ]
15
+ )
16
+ nav.run()
@@ -0,0 +1,52 @@
1
+ # {{ project_name }} — Streamlit-in-Snowflake apps
2
+
3
+ Generated by [StreamSnow](https://github.com/kyle-chalmers/streamsnow). This file is the
4
+ rulebook every Claude Code session and every developer follows. Config source of
5
+ truth: `streamsnow.config.yaml`.
6
+
7
+ ## Data access rules
8
+
9
+ - Query the `{{ gov_database }}` database. Allowed schemas (convention): {{ schema_allow | join(', ') }}.
10
+ - **Never** reference these schemas in committed app code: {{ schema_deny | join(', ') }}. This denylist is **auto-enforced**.
11
+ {% if read_exceptions %}
12
+ - Sanctioned direct-read exceptions (named columns only): {{ read_exceptions | join(', ') }}.
13
+ {% endif %}
14
+ - Don't hardcode table names you haven't verified. Discover via `INFORMATION_SCHEMA` during investigation; reference concrete objects in committed SQL.
15
+
16
+ The `check schema-refs` guardrail (pre-commit + the generated CI workflow) enforces the denylist from config — it blocks a commit/PR that references a denied schema. The allowlist above is a documented convention, not auto-enforced (avoids false positives on system schemas like `INFORMATION_SCHEMA`).
17
+
18
+ ## Runtime: {{ runtime }}
19
+
20
+ {% if runtime == 'container' %}
21
+ Container runtime (`{{ runtime_name }}`, Python {{ container_python }}). Dependencies in `pyproject.toml` (PyPI via the `{{ external_access_integration }}` external access integration). Connect with `st.connection("snowflake")` — works identically locally and deployed.
22
+ {% else %}
23
+ Warehouse runtime (legacy). Dependencies in `environment.yml` (Snowflake Anaconda channel — **never pin `python`**). Connect with `get_active_session()` wrapped in a try/except `st.connection` fallback for local dev.
24
+ {% endif %}
25
+ Query warehouse: `{{ default_warehouse }}`. Deployed apps run with caller's-rights as `{{ viewer_role }}`.
26
+
27
+ ## Caching
28
+
29
+ Every data-fetching function MUST use `@st.cache_data(ttl=<seconds>)`. Default TTL is {{ cache_ttl }}s; deviate only with a documented reason. Pass filter values as function arguments (not closures) so the cache key differentiates.
30
+
31
+ ## SQL organization
32
+
33
+ Any SQL that feeds a UI element lives in `apps/<slug>/queries/<name>.sql` (loaded via `sql_loader.py`), with a header block: `-- Query:`, `-- Feeds:`, `-- Schemas:`, `-- Params:`. Inline SQL is only for plumbing/discovery.
34
+
35
+ ## Deploy source: {{ deploy_source }}
36
+
37
+ {% if deploy_source == 'stage-copy' %}
38
+ CI pushes `apps/` to a SHA-versioned internal stage (`{{ stage_database }}.{{ stage_schema }}.{{ stage_name }}/commits/<sha>/apps/<slug>/`) and runs `CREATE OR REPLACE STREAMLIT` against that path. Snowflake never reaches out to GitHub.
39
+ {% else %}
40
+ Snowflake's `GIT REPOSITORY` pulls app source on `snow git fetch`; deploy runs `CREATE STREAMLIT ... FROM '@<repo>/branches/{{ "{" }}branch}/apps/<slug>/'` (new) or the `ABORT → PULL → COMMIT → ADD LIVE VERSION FROM LAST` refresh (existing). Requires Snowflake→GitHub network reachability.
41
+ {% endif %}
42
+ STREAMLIT objects are created in `{{ app_database }}.{{ app_schema }}`. Deploy through CI on merge to `main` — never `snow streamlit deploy` locally. (Wire the deploy workflow + your Snowflake CI credentials per the deploy docs; the generated CI workflow covers lint + schema-refs today.)
43
+
44
+ ## Number formatting
45
+
46
+ Every number shown to a user includes thousand separators (`format="localized"`/`"dollar"`/`"percent"` for column configs; `f"{n:,}"` / `f"${v:,.2f}"` inline).
47
+
48
+ ## Conventions
49
+
50
+ - App directories: `{domain}-{function}` kebab-case under `apps/`.
51
+ - Entrypoint is `streamlit_app.py` (not configurable). `st.set_page_config(...)` first, once. Multipage via `st.navigation` + `st.Page`.
52
+ - Import branding from the local copy: `from branding import ...`.
@@ -0,0 +1 @@
1
+ @AGENTS.md
@@ -0,0 +1,33 @@
1
+ # {{ project_name }}
2
+
3
+ Streamlit-in-Snowflake apps, scaffolded and governed with
4
+ [StreamSnow](https://github.com/kyle-chalmers/streamsnow).
5
+
6
+ - **Runtime:** {{ runtime }}
7
+ - **Deploy source:** {{ deploy_source }}
8
+ - **Data:** queries `{{ gov_database }}` (schemas: {{ schema_allow | join(', ') }})
9
+
10
+ ## Apps
11
+
12
+ | App | Path |
13
+ |-----|------|
14
+ | {{ app_title }} | `apps/{{ app_slug }}/` |
15
+
16
+ ## Local development
17
+
18
+ ```bash
19
+ uv venv --python 3.11
20
+ uv pip install streamsnow
21
+ cp apps/{{ app_slug }}/.streamlit/secrets.toml.example apps/{{ app_slug }}/.streamlit/secrets.toml
22
+ # edit secrets.toml with your Snowflake connection, then:
23
+ streamlit run apps/{{ app_slug }}/streamlit_app.py
24
+ ```
25
+
26
+ ## Add an app
27
+
28
+ ```bash
29
+ streamsnow new <domain> <function>
30
+ ```
31
+
32
+ Governance rules (data access, caching, SQL layout, deploy) live in [`AGENTS.md`](AGENTS.md).
33
+ Configuration is in [`streamsnow.config.yaml`](streamsnow.config.yaml).
@@ -0,0 +1,35 @@
1
+ # Governance checks for {{ project_name }} (generated by StreamSnow).
2
+ # Runs the same guardrails as the local pre-commit hooks. The deploy workflow
3
+ # is wired separately when you connect your Snowflake CI credentials.
4
+ name: checks
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ concurrency:
15
+ group: checks-${{ '{{' }} github.ref {{ '}}' }}
16
+ cancel-in-progress: true
17
+
18
+ jobs:
19
+ checks:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: astral-sh/setup-uv@v5
24
+ - name: Install tooling
25
+ run: |
26
+ uv tool install ruff
27
+ uv tool install streamsnow
28
+ - name: Lint
29
+ run: ruff check apps/
30
+ - name: Governance gate (validate every app)
31
+ run: |
32
+ for d in apps/*/; do
33
+ [ -d "$d" ] || continue
34
+ streamsnow validate-app "$(basename "$d")"
35
+ done
@@ -0,0 +1,66 @@
1
+ {% raw %}# Deploy to Snowflake on merge to main (generated by StreamSnow — git-repository).
2
+ # EXPERIMENTAL source: Snowflake must reach GitHub (snow git fetch) or you mint a
3
+ # GitHub-App token into your secret. Run `streamsnow deploy-setup` once to create
4
+ # the API integration + secret + GIT REPOSITORY. Same SNOWFLAKE_* repo secrets as
5
+ # stage-copy; until SNOWFLAKE_ACCOUNT is set this job skips.
6
+ name: deploy
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: deploy-snowflake
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ deploy:
21
+ runs-on: ubuntu-latest
22
+ env:
23
+ SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
24
+ SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
25
+ SNOWFLAKE_AUTHENTICATOR: SNOWFLAKE_JWT
26
+ SNOWFLAKE_PRIVATE_KEY_RAW: ${{ secrets.SNOWFLAKE_PRIVATE_KEY_RAW }}
27
+ SNOWFLAKE_PRIVATE_KEY_PASSPHRASE: ${{ secrets.SNOWFLAKE_PRIVATE_KEY_PASSPHRASE }}
28
+ SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }}
29
+ SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }}
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - name: Gate on configured secrets
34
+ id: gate
35
+ run: |
36
+ if [ -z "$SNOWFLAKE_ACCOUNT" ]; then
37
+ echo "Deploy secrets not set — skipping (see docs/deploy-setup.md)."
38
+ echo "go=false" >> "$GITHUB_OUTPUT"
39
+ else
40
+ echo "go=true" >> "$GITHUB_OUTPUT"
41
+ fi
42
+
43
+ - uses: astral-sh/setup-uv@v5
44
+ if: steps.gate.outputs.go == 'true'
45
+
46
+ - name: Install tooling
47
+ if: steps.gate.outputs.go == 'true'
48
+ run: |
49
+ uv tool install streamsnow
50
+ uv tool install snowflake-cli-labs
51
+
52
+ - name: Deploy changed apps (git-repository)
53
+ if: steps.gate.outputs.go == 'true'
54
+ run: |
55
+ set -euo pipefail
56
+ REPO="$(streamsnow config-get deploy.git_repository_fqn)"
57
+ snow git fetch "$REPO"
58
+ for d in apps/*/; do
59
+ [ -d "$d" ] || continue
60
+ slug="$(basename "$d")"
61
+ streamsnow deploy-sql "$slug" > "/tmp/ss-$slug.sql"
62
+ snow sql -f "/tmp/ss-$slug.sql"
63
+ streamsnow deploy-sql "$slug" --refresh > "/tmp/ss-$slug-refresh.sql"
64
+ snow sql -f "/tmp/ss-$slug-refresh.sql" || true
65
+ done
66
+ {% endraw %}
@@ -0,0 +1,68 @@
1
+ {% raw %}# Deploy to Snowflake on merge to main (generated by StreamSnow — stage-copy).
2
+ # Set these repo secrets first (docs/deploy-setup.md). Until SNOWFLAKE_ACCOUNT is
3
+ # set the job SKIPS, so it never red-X's a normal merge:
4
+ # SNOWFLAKE_ACCOUNT SNOWFLAKE_USER SNOWFLAKE_PRIVATE_KEY_RAW
5
+ # SNOWFLAKE_PRIVATE_KEY_PASSPHRASE (optional) SNOWFLAKE_WAREHOUSE SNOWFLAKE_ROLE
6
+ name: deploy
7
+
8
+ on:
9
+ push:
10
+ branches: [main]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: deploy-snowflake
17
+ cancel-in-progress: false
18
+
19
+ jobs:
20
+ deploy:
21
+ runs-on: ubuntu-latest
22
+ env:
23
+ SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
24
+ SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
25
+ SNOWFLAKE_AUTHENTICATOR: SNOWFLAKE_JWT
26
+ SNOWFLAKE_PRIVATE_KEY_RAW: ${{ secrets.SNOWFLAKE_PRIVATE_KEY_RAW }}
27
+ SNOWFLAKE_PRIVATE_KEY_PASSPHRASE: ${{ secrets.SNOWFLAKE_PRIVATE_KEY_PASSPHRASE }}
28
+ SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }}
29
+ SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }}
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - name: Gate on configured secrets
34
+ id: gate
35
+ run: |
36
+ if [ -z "$SNOWFLAKE_ACCOUNT" ]; then
37
+ echo "Deploy secrets not set — skipping (see docs/deploy-setup.md)."
38
+ echo "go=false" >> "$GITHUB_OUTPUT"
39
+ else
40
+ echo "go=true" >> "$GITHUB_OUTPUT"
41
+ fi
42
+
43
+ - uses: astral-sh/setup-uv@v5
44
+ if: steps.gate.outputs.go == 'true'
45
+
46
+ - name: Install tooling
47
+ if: steps.gate.outputs.go == 'true'
48
+ run: |
49
+ uv tool install streamsnow
50
+ uv tool install snowflake-cli-labs
51
+
52
+ - name: Deploy changed apps (stage-copy)
53
+ if: steps.gate.outputs.go == 'true'
54
+ run: |
55
+ set -euo pipefail
56
+ STAGE="$(streamsnow stage-path)"
57
+ streamsnow deploy-setup > /tmp/ss-setup.sql && snow sql -f /tmp/ss-setup.sql
58
+ snow stage copy "apps/" "$STAGE/commits/$GITHUB_SHA/apps/" --recursive --overwrite
59
+ for d in apps/*/; do
60
+ cfg="${d}.streamlit/config.toml"
61
+ [ -f "$cfg" ] && snow stage copy "$cfg" "$STAGE/commits/$GITHUB_SHA/apps/$(basename "$d")/.streamlit/" --overwrite
62
+ done
63
+ for d in apps/*/; do
64
+ slug="$(basename "$d")"
65
+ streamsnow deploy-sql "$slug" --sha "$GITHUB_SHA" > "/tmp/ss-$slug.sql"
66
+ snow sql -f "/tmp/ss-$slug.sql"
67
+ done
68
+ {% endraw %}
@@ -0,0 +1,21 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ venv/
7
+
8
+ # Tooling
9
+ .ruff_cache/
10
+ .pytest_cache/
11
+
12
+ # Secrets — NEVER commit
13
+ **/.streamlit/secrets.toml
14
+
15
+ # Review/walkthrough artifacts (screenshots, reports)
16
+ apps/*/.review/
17
+
18
+ # OS / editor
19
+ .DS_Store
20
+ .idea/
21
+ .vscode/
@@ -0,0 +1,42 @@
1
+ # Pre-commit hooks for {{ project_name }}. Install with: pre-commit install
2
+ # Requires: pip/uv install streamsnow (provides the `streamsnow` CLI).
3
+ repos:
4
+ - repo: https://github.com/astral-sh/ruff-pre-commit
5
+ rev: v0.15.9
6
+ hooks:
7
+ - id: ruff-check
8
+ args: [--fix]
9
+ - id: ruff-format
10
+
11
+ - repo: https://github.com/Yelp/detect-secrets
12
+ rev: v1.5.0
13
+ hooks:
14
+ - id: detect-secrets
15
+
16
+ - repo: local
17
+ hooks:
18
+ # All four require the `streamsnow` package on PATH (uv/pip install streamsnow).
19
+ - id: streamsnow-schema-refs
20
+ name: streamsnow check schema-refs (block denied schemas)
21
+ entry: streamsnow check schema-refs
22
+ language: system
23
+ files: ^apps/.*\.(py|sql)$
24
+ pass_filenames: true
25
+ - id: streamsnow-security
26
+ name: streamsnow check security (no egress/exec/write-SQL)
27
+ entry: streamsnow check security
28
+ language: system
29
+ files: ^apps/.*\.(py|sql)$
30
+ pass_filenames: true
31
+ - id: streamsnow-bind-predicates
32
+ name: streamsnow check bind-predicates (Go-driver trap)
33
+ entry: streamsnow check bind-predicates
34
+ language: system
35
+ files: ^apps/.*\.(py|sql)$
36
+ pass_filenames: true
37
+ - id: streamsnow-caching
38
+ name: streamsnow check caching (@st.cache_data ttl required)
39
+ entry: streamsnow check caching
40
+ language: system
41
+ files: ^apps/.*\.py$
42
+ pass_filenames: true