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
8 changes: 4 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ updates:
# .pre-commit-config.yaml, and the two move together by hand. A
# Dependabot bump of the cap alone splits CI from the local hook.
# Name-only on purpose: an update-types ignore is computed from the first
# X.Y.Z version in the requirement, and `>=24.0,<25.10` has none, so it
# X.Y.Z version in the requirement, and `>=26.5,<26.6` has none, so it
# would match nothing. Security updates are not blocked by it: in
# dependabot-core, IgnoreCondition#ignored_versions returns early for
# security_updates_only with just the explicit `versions`, and a
# name-only ignore has none. Black's security fixes since 25.9.0 need
# Python 3.10+, so a security PR for it is a decision about the 3.9
# floor and the hook rev, not a plain merge.
# name-only ignore has none. Security updates are off for this repo,
# though, so a black advisory has to be caught by hand, and the fix
# moves the hook rev in .pre-commit-config.yaml alongside the cap.
- dependency-name: "black"

# GitHub Actions used by the CI and publish workflows.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
7 changes: 3 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ repos:
- id: ruff-check

- repo: https://github.com/psf/black
# Kept in sync with the [dev] cap in pyproject.toml (<25.10); move both
# together by hand. Stay on a black that runs on Python 3.9 (the project's
# runtime floor): 25.11.0 is the last one, 25.12.0+ require 3.10.
rev: 25.9.0
# Kept in sync with the [dev] cap in pyproject.toml (<26.6); move both
# together by hand.
rev: 26.5.1
hooks:
- id: black

Expand Down
11 changes: 1 addition & 10 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,6 @@ the linked regression test first.
(`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
Expand All @@ -140,7 +132,7 @@ the linked regression test first.
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 across Python 3.9-3.13. Run with
- Target coverage: >= 89%, enforced across Python 3.11-3.15. Run with
`coverage run -m pytest && coverage report` (see Known Quirks).

## Common Mistakes
Expand All @@ -153,7 +145,6 @@ the linked regression test first.
- 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.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ All four must pass (and `pre-commit run --all-files` is clean). Use `coverage ru
(`snowflake_sql_api.testing`), and `coverage run` starts tracing before that
plugin is imported so import-time lines are measured correctly. The coverage gate
is enforced (`fail_under = 89` in `pyproject.toml`); `coverage report` exits
non-zero below it. CI runs the same checks across Python 3.9-3.13.
non-zero below it. CI runs the same checks across Python 3.11-3.15.

Checklist:

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pip install "snowflake-sql-api[pandas]" # DataFrame output helpers
pip install "snowflake-sql-api[pydantic]" # typed-row mapping
```

Requires Python 3.9 or newer. Core dependencies are `httpx`, `PyJWT`, and
Requires Python 3.11 or newer. Core dependencies are `httpx`, `PyJWT`, and
`cryptography`.

## Prerequisites
Expand Down
36 changes: 17 additions & 19 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "snowflake-sql-api"
dynamic = ["version"]
description = "Lightweight, pure-Python client for Snowflake's SQL API v2"
readme = "README.md"
requires-python = ">=3.9"
requires-python = ">=3.11"
license = { file = "LICENSE" }
authors = [{ name = "hampsterx" }]
keywords = ["snowflake", "sql", "rest", "api", "httpx", "serverless", "lambda"]
Expand All @@ -17,11 +17,11 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3.15",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Database",
"Typing :: Typed",
Expand All @@ -41,12 +41,10 @@ dev = [
"pytest-asyncio>=0.21",
"respx>=0.20",
"ruff>=0.4",
# Capped below 25.10 so CI formats with the same black the pre-commit hook
# pins (rev 25.9.0 in .pre-commit-config.yaml). Move the two together by
# hand; Dependabot ignores black (.github/dependabot.yml). Keep the cap at
# or below 25.12 while Python 3.9 is supported: 25.12.0+ require 3.10, so
# a higher cap resolves a different black per interpreter.
"black>=24.0,<25.10",
# Held to 26.5.x so CI formats with the same black the pre-commit hook
# pins (rev 26.5.1 in .pre-commit-config.yaml). Move the two together by
# hand; Dependabot ignores black (.github/dependabot.yml).
"black>=26.5,<26.6",
"mypy>=1.8",
"build>=1.0",
"pre-commit>=3.5",
Expand Down Expand Up @@ -89,28 +87,28 @@ include = [

[tool.ruff]
line-length = 88
target-version = "py39"
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "W", "I", "B", "C4", "SIM"]
# E501 (line length) is owned by black. `UP` (pyupgrade) is omitted: it would
# rewrite runtime type aliases like `Optional[Sequence[Any]]` to `X | None`,
# which raises at import time on Python 3.9 (PEP 604 on typing generics is 3.10+).
# E501 (line length) is owned by black. `UP` (pyupgrade) is not enabled yet: the
# code predates the 3.11 floor and still spells `Optional`/`List`, so turning it
# on is a ~250-site annotation rewrite of its own.
ignore = ["E501"]

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"]

[tool.black]
line-length = 88
# Pin to py39 grammar: our code uses no newer syntax, and a single floor target
# keeps black's AST safety check parseable by the interpreter running CI.
target-version = ["py39"]
# Pin to the floor's grammar: a single floor target keeps black's AST safety
# check parseable by the interpreter running CI.
target-version = ["py311"]

[tool.mypy]
# Lowest target this mypy accepts; true 3.9 runtime compat is gated by the
# pytest matrix (3.9-3.13), which imports every module under each interpreter.
python_version = "3.10"
# The runtime floor; the pytest matrix (3.11-3.15) imports every module under
# each supported interpreter.
python_version = "3.11"
files = ["snowflake_sql_api"]
disallow_untyped_defs = true
disallow_incomplete_defs = true
Expand Down
5 changes: 4 additions & 1 deletion snowflake_sql_api/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,9 @@ def coerce_rows(raw_rows: List[List[Any]], columns: List[ColumnMeta]) -> List[Ro
"""Coerce a partition of raw rows into a list of name-keyed :data:`Row`."""
names = [c.name for c in columns]
return [
{name: coerce_value(cell, col) for name, col, cell in zip(names, columns, row)}
{
name: coerce_value(cell, col)
for name, col, cell in zip(names, columns, row, strict=False)
}
for row in raw_rows
]
4 changes: 1 addition & 3 deletions tests/test_import_footprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ def exec_module(self, module):
sys.meta_path.insert(0, _Recorder())
import snowflake_sql_api # noqa: F401
print(",".join(sorted(set(attempted))))
""".format(
forbidden=FORBIDDEN_ON_BARE_IMPORT
)
""".format(forbidden=FORBIDDEN_ON_BARE_IMPORT)

_TIMING_SCRIPT = """
import time
Expand Down
2 changes: 0 additions & 2 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,6 @@ def test_register_match_predicate() -> None:


def test_unregistered_statement_raises() -> None:
# Single `with` per statement here: a parenthesized multi-context `with`
# is 3.10+ syntax and breaks the 3.9 leg of the test matrix.
fake = FakeSnowflake()
client = make_client(fake)
with pytest.raises(FakeSnowflakeError, match="no result registered"):
Expand Down
Loading