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.
- streamsnow/__init__.py +19 -0
- streamsnow/_templates/app/AGENTS.md.j2 +15 -0
- streamsnow/_templates/app/branding.py.j2 +51 -0
- streamsnow/_templates/app/config.toml.j2 +8 -0
- streamsnow/_templates/app/environment.yml.j2 +11 -0
- streamsnow/_templates/app/example_metric.sql.j2 +12 -0
- streamsnow/_templates/app/overview.py.j2 +48 -0
- streamsnow/_templates/app/pyproject.toml.j2 +12 -0
- streamsnow/_templates/app/secrets.toml.example.j2 +10 -0
- streamsnow/_templates/app/snowflake.yml.j2 +29 -0
- streamsnow/_templates/app/sql_loader.py.j2 +21 -0
- streamsnow/_templates/app/streamlit_app.py.j2 +16 -0
- streamsnow/_templates/repo/AGENTS.md.j2 +52 -0
- streamsnow/_templates/repo/CLAUDE.md.j2 +1 -0
- streamsnow/_templates/repo/README.md.j2 +33 -0
- streamsnow/_templates/repo/ci.yml.j2 +35 -0
- streamsnow/_templates/repo/deploy.git.yml.j2 +66 -0
- streamsnow/_templates/repo/deploy.yml.j2 +68 -0
- streamsnow/_templates/repo/gitignore.j2 +21 -0
- streamsnow/_templates/repo/pre-commit-config.yaml.j2 +42 -0
- streamsnow/cli.py +594 -0
- streamsnow/config.py +401 -0
- streamsnow/deploy.py +152 -0
- streamsnow/policy.py +48 -0
- streamsnow/scaffolder.py +188 -0
- streamsnow/tools/__init__.py +8 -0
- streamsnow/tools/check_app_security.py +643 -0
- streamsnow/tools/check_bind_predicates.py +66 -0
- streamsnow/tools/check_caching.py +186 -0
- streamsnow/tools/check_export_clean.py +123 -0
- streamsnow/tools/check_schema_refs.py +272 -0
- streamsnow/tools/validate_app.py +281 -0
- streamsnow-0.1.0.dist-info/METADATA +149 -0
- streamsnow-0.1.0.dist-info/RECORD +37 -0
- streamsnow-0.1.0.dist-info/WHEEL +4 -0
- streamsnow-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|