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
35 changes: 35 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: Bug report
about: Report incorrect behaviour
title: ""
labels: bug
assignees: ""
---

## What happened

A clear description of the bug.

## Expected behaviour

What you expected instead.

## Reproduction

A minimal snippet. Mock the client with `snowflake_sql_api.testing` if the issue
does not need a live account (see docs/testing.md):

```python
# ...
```

## Environment

- `snowflake-sql-api` version:
- Python version:
- OS:

## Additional context

Tracebacks (redact secrets), Snowflake `code` / `sqlState` if relevant, anything
else that helps.
24 changes: 24 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: Feature request
about: Suggest an enhancement
title: ""
labels: enhancement
assignees: ""
---

## Problem

What are you trying to do that the library does not support today?

## Proposed solution

What you would like to see. Keep in mind the project's design principles
(pure-Python, small footprint, vendor-neutral, sync/async parity).

## Alternatives considered

Other approaches, existing workarounds, or related libraries.

## Additional context

Anything else (links to Snowflake SQL API docs, example use case).
22 changes: 22 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## Summary

What changed and why. Write for the reviewer and for downstream consumers, not
for yourself: lead with the user-visible behaviour, not the implementation
narrative.

## Backwards compatibility

This is a library; downstream code depends on it. State the impact:

- [ ] No public API change, or
- [ ] Public API changed (describe it, and the migration for callers)

Do not add compatibility shims for behaviour that never shipped; just change the
code.

## Test plan

- [ ] `coverage run -m pytest && coverage report` passes (coverage holds >= 89%)
- [ ] `ruff check`, `black --check`, and `mypy` pass (or `pre-commit run --all-files`)
- [ ] New behaviour is covered by tests; each fixed bug has a `test_regression_*`
- [ ] Public API changes documented in the README / `docs/`
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: 2
updates:
# Python dependencies declared in pyproject.toml.
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"

# GitHub Actions used by the CI and publish workflows.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
Expand All @@ -38,6 +40,8 @@ jobs:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
Expand All @@ -46,5 +50,10 @@ jobs:
python -m pip install --upgrade pip
pip install -e '.[dev]'
- name: pytest
# Coverage gate raised to >= 89% from Phase 2 (--cov-fail-under).
run: pytest --cov=snowflake_sql_api --cov-report=term-missing
# Use `coverage run` (not `pytest --cov`) so tracing starts before the
# package's own pytest11 plugin (snowflake_sql_api.testing) is imported;
# otherwise import-time lines read as uncovered. The 89% gate lives in
# pyproject.toml [tool.coverage.report] fail_under.
run: |
coverage run -m pytest
coverage report
82 changes: 82 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Publish to PyPI

# Tag-driven release. Push a `vX.Y.Z` tag and this builds the sdist/wheel
# (version derived from the tag by hatch-vcs) and uploads to PyPI via OIDC
# trusted publishing. See RELEASING.md.
on:
push:
tags:
- "v*"

jobs:
lint:
name: Lint and type-check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e '.[dev]'
- name: ruff
run: ruff check snowflake_sql_api tests
- name: black
run: black --check snowflake_sql_api tests
- name: mypy
run: mypy snowflake_sql_api

test:
name: Test (py${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e '.[dev]'
- name: pytest
# See CI: `coverage run` so tracing precedes the pytest11 plugin import.
run: |
coverage run -m pytest
coverage report

publish:
name: Build and publish
needs: [lint, test]
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # OIDC trusted publishing; no API token needed
steps:
- uses: actions/checkout@v4
with:
# hatch-vcs needs the tag (and history) to derive the version.
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build tooling
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build sdist and wheel
run: python -m build
- name: Check distribution metadata
run: twine check dist/*
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
39 changes: 39 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Pre-commit hooks for snowflake-sql-api. Install with `pre-commit install`
# (the hook then runs on every commit); run the full set with
# `pre-commit run --all-files`. Hook versions track the `[dev]` floors in
# pyproject.toml.
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.17
hooks:
- id: ruff-check

- repo: https://github.com/psf/black
# Last black that still runs on Python 3.9 (the project's runtime floor);
# 25.12.0+ require 3.10+. Kept in sync with the [dev] cap in pyproject.toml.
rev: 25.9.0
hooks:
- id: black

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-toml
- id: check-merge-conflict
- id: check-added-large-files
# Keypair auth: belt-and-braces with the *.p8 / *.pem .gitignore rules.
- id: detect-private-key

# mypy runs as a local hook (not the mirror) so it resolves project imports
# and stubs from the installed dev environment, matching the CI invocation.
- repo: local
hooks:
- id: mypy
name: mypy
entry: mypy snowflake_sql_api
language: system
types: [python]
pass_filenames: false
89 changes: 82 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Module layout under `snowflake_sql_api/` (clean separation, `py.typed`):
| `aclient.py` | Asynchronous client (same surface, `await`-based) |
| `row_mapping.py` | Optional dataclass / Pydantic row mapping |
| `cli.py` | Command-line interface (`snowflake-sql-api query ...`) |
| `testing.py` | Shipped test helper: `FakeSnowflake` (httpx.MockTransport), `make_client`/`make_async_client`, pytest fixtures (pytest11) |

### Dependencies

Expand All @@ -52,21 +53,23 @@ than failing at import time.
## Development Commands

```bash
# Install in editable mode with dev tooling
# Install in editable mode with dev tooling, then wire the git hooks
pip install -e '.[dev]'
pre-commit install

# Run tests with coverage
pytest --cov=snowflake_sql_api --cov-report=term-missing
# Run tests with coverage (NOT `pytest --cov`; see Known Quirks)
coverage run -m pytest && coverage report

# Run only the known-bug regression tests
pytest -k regression

# Lint / format / type-check
# Lint / format / type-check (or run all hooks at once)
ruff check snowflake_sql_api tests
black --check snowflake_sql_api tests
mypy snowflake_sql_api
pre-commit run --all-files

# Build a distribution
# Build a distribution (version comes from the git tag via hatch-vcs)
python -m build
```

Expand All @@ -89,18 +92,90 @@ private keys, `.gitignore` excludes `*.pem` / `*.p8` / `*private_key*`.
- **Correctness over surface.** Type coercion and partition handling are
correctness-critical, prefer well-tested core behavior to breadth.

## Known Quirks

Behaviour that looks wrong but is intentional. Do not "fix" these without reading
the linked regression test first.

- **Account locator: claim vs host** (`auth.py`). The JWT claim account
(`iss`/`sub`) strips the region/cloud suffix and uppercases
(`xy12345.ap-southeast-2` -> `XY12345`); the API host keeps the full account
(`xy12345.ap-southeast-2.snowflakecomputing.com`). Conflating them breaks JWT
validation. `normalize_account_locator` vs `account_hostname`. Regression:
`test_regression_bug1`.
- **`result(poll=False)` raises on 202** (`client.py` / `aclient.py`
`_collect`). A still-running async statement must raise `ResultNotReady`, never
return its in-progress HTTP 202 body as if it were a result set. Regression:
`test_regression_bug3`.
- **Fetch every partition, in order** (`pagination.py`). `query` returns
partition 0 (inline) plus partitions 1..N (fetched by index). Stopping at
partition 0 silently truncates large results. Regression:
`test_regression_bug4`.
- **`on_query` streaming hook is deferred** to the v0.2.0 toolkit
(`query_stream`, Phase 8). The hook fires for `query`/`execute`/`submit` today;
there is no streaming path yet, so no regression test until the feature lands
(this is spike bug #2, intentionally not yet covered).
- **No PEP 604 unions at runtime** (py3.9 floor). ruff's `UP` (pyupgrade) rule is
omitted on purpose: it would rewrite `Optional[...]` / `Union[...]` to
`X | None`, which raises at import time on 3.9 for typing generics (PEP 604 on
generics is 3.10+). Keep `from __future__ import annotations` plus
`Optional`/`Union`.
- **mypy `python_version = "3.10"` vs the 3.9-3.13 matrix.** 3.10 is the lowest
this mypy accepts; true 3.9 runtime compatibility is enforced by the pytest
matrix, which imports every module under 3.9.
- **Coverage uses `coverage run`, not `pytest --cov`.** The package ships a pytest
plugin via the `pytest11` entry point, so `snowflake_sql_api.testing` (and the
whole package) is imported at plugin-load time, before pytest-cov starts
tracing. `pytest --cov` then reports import-time lines as uncovered (~20 points
lost). `coverage run -m pytest` starts tracing first. The 89% gate lives in
`pyproject.toml` `[tool.coverage.report] fail_under`.

## Testing

- Unit tests mock the HTTP layer; no network access required for the default suite.
- Mock the client in your own tests with the shipped `snowflake_sql_api.testing`
helper (`FakeSnowflake` + `make_client`/`make_async_client`, or the
auto-registered `fake_snowflake` / `snowflake_client` / `async_snowflake_client`
fixtures). No respx. See `docs/testing.md`.
- Each fixed bug gets a named regression test (`test_regression_*`) so it cannot
silently return.
- Target coverage: >= 89%, enforced in CI across Python 3.9-3.13.
- Target coverage: >= 89%, enforced across Python 3.9-3.13. Run with
`coverage run -m pytest && coverage report` (see Known Quirks).

## Common Mistakes

- Hand-editing a version string. The version comes from the git tag (hatch-vcs);
`_version.py` is generated and gitignored. A feature PR must not touch it. See
`RELEASING.md`.
- Running `pytest --cov` and reacting to the false coverage drop. Use
`coverage run -m pytest`.
- Adding a runtime dependency without strong justification. The small
install / fast cold start is the whole point; new optional features go behind
an extra.
- Rewriting `Optional[...]` to `X | None` (breaks the 3.9 runtime).
- Conflating the JWT claim account with the API host (see Known Quirks).
- Forgetting the async counterpart of a sync change (sync/async parity).
- Forgetting a `test_regression_*` for a fixed bug.

## Before Finishing

1. `pre-commit run --all-files` is clean (ruff, black, mypy, yaml/toml, private-key).
2. `coverage run -m pytest && coverage report` passes and coverage holds >= 89%.
3. Sync/async parity: any client change has its counterpart, or a stated reason.
4. Fixed bugs have a `test_regression_*`; public API changes are in the
README / `docs/`.

## Security

Report vulnerabilities privately, see [SECURITY.md](SECURITY.md). Never commit
private keys (`.gitignore` and a `detect-private-key` pre-commit hook guard
this).

## Contributing

Before opening a PR:

- [ ] Tests pass (`pytest --cov`) and coverage holds.
- [ ] Tests pass (`coverage run -m pytest && coverage report`) and coverage holds.
- [ ] Formatted (`black`) and linted (`ruff`), type hints on public APIs (`mypy`).
- [ ] No hardcoded account/region/role/warehouse values; configuration is generic.
- [ ] Public API changes documented in the README.
Expand Down
Loading
Loading