feat(streamlit): resolve STREAMLIT grants at plan time (fetch_streamlit)#24
feat(streamlit): resolve STREAMLIT grants at plan time (fetch_streamlit)#24usbrandon wants to merge 6 commits into
Conversation
STREAMLIT was a supported schema-scoped resource for rendering grant SQL,
but `snowcap plan` errored when a grant referenced a streamlit object:
module 'snowcap.data_provider' has no attribute 'fetch_streamlit'
Because Streamlit has a concrete resource class (unlike CORTEX SEARCH SERVICE,
which is SchemaScope-only), resolving a `on: streamlit <db>.<schema>.<app>`
grant reference drives fetch_resource -> fetch_streamlit, which didn't exist.
This adds it so grants on streamlit apps can be planned and applied:
grants:
- priv: USAGE
on: streamlit <db>.<schema>.<app>
to: <viewer_role>
- snowcap/data_provider.py: fetch_streamlit() — SHOW STREAMLITS + DESC
STREAMLIT, mapping root_location -> from_, plus main_file / title /
query_warehouse / comment / owner (mirrors fetch_notebook).
- tests/test_grant.py: test_grant_on_streamlit (USAGE renders correctly).
- tests/test_privs.py: TestStreamlitPriv (enum values).
- docs/resources/streamlit.md: "Granting access" section.
- docs/resources/grant.md: streamlit example in the Object Grants block.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016L3b4GTh5nyFy5vxQ6e9TY
|
Thanks for this, @usbrandon — clean, well-scoped fix and the live validation is appreciated. Modeling it on Requested changes
Nits
Looks good
No concern about the lack of a direct |
|
All requested changes are in, plus live validation against a real account. Requested changes
Live validation (short-form
Also fixed a pre-existing blocker: Nits: added the trailing newline to |
The four ACCOUNT_USAGE caches in data_provider are keyed by id(session), but
the test classes covering them cleared only _ACCOUNT_USAGE_ACCESS_CACHE and
_ACCOUNT_USAGE_FALLBACK_CACHE, leaving _ACCOUNT_USAGE_GRANTS_CACHE and
_ACCOUNT_USAGE_USER_GRANTS_CACHE populated. CPython reuses the address of a
freed object, so a fresh MagicMock session frequently lands on the previous
test's address and inherits its cached grants (measured: 169 id() collisions
per 200 sequential MagicMocks).
When that collision happened, test_returns_none_on_access_control_error hit
the grant list cached by test_returns_normalized_grants and returned early,
never reaching the except branch, failing with:
assert [{'created_on': ..., 'privilege': 'SELECT', ...}] is None
This is allocation-dependent, which is why it surfaced only on build (3.12).
Replace the six partial setup_method clears with one autouse fixture in the
root conftest that calls the existing reset_account_usage_caches() helper, so
all four caches are reset around every test in every file (pytest runs with
--dist loadfile, so a worker can host several files).
Verified on Python 3.12.12 and 3.13.9: with the address collision forced, the
assertion fails before this change and passes after; ruff, mypy and the full
1537-test suite pass on both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dev extra declared a bare "ruff", so CI installs whatever is current. ruff 0.16.0 expanded the default rule set (I001, RUF100, UP0xx, SIM1xx, TRY0xx and more), which reports 723 errors on snowcap/ as it stands on main and 725 on this branch. `ruff check snowcap/` is the first command in the CI check step, so every job now fails before the type check or tests run -- this is repo-wide, not specific to any branch. main only looks green because its last run predates the 0.16 release. Verified against a pristine origin/main tree: 723 errors under 0.16.0, zero under 0.15.22. Pin below 0.16 to restore the linter CI was passing with. The durable alternative is to declare the rule selection explicitly under [tool.ruff.lint] in pyproject.toml so future ruff releases cannot change what is enforced; left to the maintainers. Verified on Python 3.10.18, 3.11.13, 3.12.12 and 3.13.9: a fresh `pip install -e ".[dev]"` resolves ruff 0.15.22, and ruff, mypy and all 1537 tests pass on every version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed two fixes for the failing checks. Neither failure originated in this PR — details below in case they're useful beyond this branch. 1.
|
| tree | ruff 0.15.22 | ruff 0.16.0 |
|---|---|---|
origin/main |
All checks passed | 723 errors |
| this branch | All checks passed | 725 errors |
main looks green only because its last Run Tests predates the 0.16 release. Any PR opened today hits this.
Fix: pin ruff<0.16 in the dev extra, restoring the linter CI was passing with. The durable alternative is declaring the rule selection explicitly under [tool.ruff.lint] in pyproject.toml so future ruff releases can't change what's enforced — happy to do that instead if you'd prefer; it's your lint policy, so I left it alone.
Verification
Real venvs per interpreter, running CI's exact commands:
| ruff | mypy | pytest | forced id() collision (before → after) |
|
|---|---|---|---|---|
| 3.10.18 | pass | 34 files clean | 1537 passed | fails → passes |
| 3.11.13 | pass | 34 files clean | 1537 passed | fails → passes |
| 3.12.12 | pass | 34 files clean | 1537 passed | fails → passes |
| 3.13.9 | pass | 34 files clean | 1537 passed | fails → passes |
3.13 included since python_requires=">=3.10" has no upper bound — you may want "3.13" in the run-tests.yml matrix, but I didn't change the workflow here.
One thing I did not fix
Keying those caches by id(session) is also a production hazard, not just a test artifact: nothing holds a reference to the session, so a garbage-collected connection leaves its grant list behind, and a new connection allocated at the same address inherits it — stale grants at plan time. Out of scope here; happy to open a separate PR (weakref-keyed caches across the 8 session_id = id(session) sites) if you want it.
|
Thanks @usbrandon — reviewed and this looks solid. Confirmed the key pieces: the One small note before merge: making |
What
snowcap planerrors whenever a grant references a Streamlit app:StreamlitPriv,SchemaPriv.CREATE_STREAMLIT, and theStreamlitresource class already exist, so grant SQL renders — but because Streamlit has a concrete resource class, resolvingon: streamlit <db>.<schema>.<app>drivesfetch_resource→fetch_streamlit, which is missing.Change
snowcap/data_provider.py: addfetch_streamlit()—SHOW STREAMLITS+DESC STREAMLIT, mappingroot_location→from_, plusmain_file/title/query_warehouse/comment/owner(mirrorsfetch_notebook).tests/test_grant.py:test_grant_on_streamlit.tests/test_privs.py:TestStreamlitPriv.docs/resources/streamlit.md: "Granting access" section;docs/resources/grant.md: streamlit example.Validation
Verified end-to-end against a live account:
GRANT USAGE ON STREAMLITnow plans and applies (the previously-fatal fetch error is gone; the grant shows as a normal create). Full unit suite green.🤖 Generated with Claude Code