snowflake-sql-api 0.1.0__tar.gz → 0.1.1__tar.gz
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.
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/PKG-INFO +2 -2
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/pyproject.toml +4 -4
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/_version.py +2 -2
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/cli.py +1 -1
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/testing.py +6 -2
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/conftest.py +6 -0
- snowflake_sql_api-0.1.1/tests/test_cli.py +143 -0
- snowflake_sql_api-0.1.1/tests/test_import_footprint.py +108 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_testing.py +2 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/.gitignore +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/CONTRIBUTING.md +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/LICENSE +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/README.md +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/__init__.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/aclient.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/auth.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/bindings.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/client.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/escaping.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/exceptions.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/pagination.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/py.typed +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/retry.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/row_mapping.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/transport.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/snowflake_sql_api/types.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/__init__.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/smoke/__init__.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/smoke/test_smoke.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/support.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_aclient.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_auth.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_bindings.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_client.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_escaping.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_package.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_regression.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_retry.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_transport.py +0 -0
- {snowflake_sql_api-0.1.0 → snowflake_sql_api-0.1.1}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: snowflake-sql-api
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Lightweight, pure-Python client for Snowflake's SQL API v2
|
|
5
5
|
Project-URL: Homepage, https://github.com/hampsterx/snowflake-sql-api
|
|
6
6
|
Project-URL: Repository, https://github.com/hampsterx/snowflake-sql-api
|
|
@@ -47,7 +47,7 @@ Requires-Dist: cryptography>=3.4
|
|
|
47
47
|
Requires-Dist: httpx>=0.24
|
|
48
48
|
Requires-Dist: pyjwt>=2.4
|
|
49
49
|
Provides-Extra: dev
|
|
50
|
-
Requires-Dist: black<25.
|
|
50
|
+
Requires-Dist: black<25.12,>=24.0; extra == 'dev'
|
|
51
51
|
Requires-Dist: build>=1.0; extra == 'dev'
|
|
52
52
|
Requires-Dist: coverage[toml]>=7.0; extra == 'dev'
|
|
53
53
|
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
@@ -43,7 +43,7 @@ dev = [
|
|
|
43
43
|
"ruff>=0.4",
|
|
44
44
|
# Capped below 25.10: 25.12.0+ drop Python 3.9, which the package still
|
|
45
45
|
# supports. Keeps CI and the pinned pre-commit hook on the same black.
|
|
46
|
-
"black>=24.0,<25.
|
|
46
|
+
"black>=24.0,<25.12",
|
|
47
47
|
"mypy>=1.8",
|
|
48
48
|
"build>=1.0",
|
|
49
49
|
"pre-commit>=3.5",
|
|
@@ -134,9 +134,9 @@ markers = [
|
|
|
134
134
|
[tool.coverage.run]
|
|
135
135
|
source = ["snowflake_sql_api"]
|
|
136
136
|
branch = true
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
omit = ["snowflake_sql_api/
|
|
137
|
+
# row_mapping.py is a Phase 8 stub, out of the Phase 2 core-coverage scope.
|
|
138
|
+
# cli.py is covered by tests/test_cli.py.
|
|
139
|
+
omit = ["snowflake_sql_api/row_mapping.py"]
|
|
140
140
|
|
|
141
141
|
[tool.coverage.report]
|
|
142
142
|
show_missing = true
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -68,7 +68,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
|
68
68
|
return 2
|
|
69
69
|
if args.command == "query":
|
|
70
70
|
return _run_query(args.sql)
|
|
71
|
-
return 0
|
|
71
|
+
return 0 # pragma: no cover - argparse only yields None or a known subcommand
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
if __name__ == "__main__": # pragma: no cover
|
|
@@ -599,7 +599,7 @@ def make_client(
|
|
|
599
599
|
from .client import SnowflakeClient
|
|
600
600
|
|
|
601
601
|
_reject_managed_kwargs("make_client", kwargs)
|
|
602
|
-
|
|
602
|
+
client = SnowflakeClient(
|
|
603
603
|
account,
|
|
604
604
|
user,
|
|
605
605
|
private_key=_throwaway_key(),
|
|
@@ -607,6 +607,8 @@ def make_client(
|
|
|
607
607
|
poll_interval=poll_interval,
|
|
608
608
|
**kwargs,
|
|
609
609
|
)
|
|
610
|
+
client._transport._owns_client = True
|
|
611
|
+
return client
|
|
610
612
|
|
|
611
613
|
|
|
612
614
|
def make_async_client(
|
|
@@ -621,7 +623,7 @@ def make_async_client(
|
|
|
621
623
|
from .aclient import AsyncSnowflakeClient
|
|
622
624
|
|
|
623
625
|
_reject_managed_kwargs("make_async_client", kwargs)
|
|
624
|
-
|
|
626
|
+
client = AsyncSnowflakeClient(
|
|
625
627
|
account,
|
|
626
628
|
user,
|
|
627
629
|
private_key=_throwaway_key(),
|
|
@@ -629,6 +631,8 @@ def make_async_client(
|
|
|
629
631
|
poll_interval=poll_interval,
|
|
630
632
|
**kwargs,
|
|
631
633
|
)
|
|
634
|
+
client._transport._owns_client = True
|
|
635
|
+
return client
|
|
632
636
|
|
|
633
637
|
|
|
634
638
|
# ---------------------------------------------------------------------------
|
|
@@ -25,6 +25,12 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
|
25
25
|
PASSPHRASE = b"correct horse battery staple"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
29
|
+
"""Load shipped testing fixtures when pytest11 entry points are unavailable."""
|
|
30
|
+
if not config.pluginmanager.hasplugin("snowflake_sql_api"):
|
|
31
|
+
config.pluginmanager.import_plugin("snowflake_sql_api.testing")
|
|
32
|
+
|
|
33
|
+
|
|
28
34
|
@pytest.fixture
|
|
29
35
|
def fake_account() -> str:
|
|
30
36
|
"""A region-suffixed account locator for auth-normalization tests."""
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Unit tests for the ``snowflake-sql-api`` CLI (``cli.py``).
|
|
2
|
+
|
|
3
|
+
Drive ``build_parser()`` / ``main()`` with ``argv`` injection and a stubbed
|
|
4
|
+
client (``from_env`` monkeypatched to a :class:`FakeSnowflake`-backed client), so
|
|
5
|
+
no network and no real account. Covers the exit-code contract documented in
|
|
6
|
+
``docs/cli.md`` plus the ``_json_default`` serializer.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from datetime import date, datetime, time, timezone
|
|
13
|
+
from decimal import Decimal
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from snowflake_sql_api import cli
|
|
18
|
+
from snowflake_sql_api.client import SnowflakeClient
|
|
19
|
+
from snowflake_sql_api.testing import FakeSnowflake, make_client
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def stub_from_env(monkeypatch: pytest.MonkeyPatch) -> FakeSnowflake:
|
|
24
|
+
"""Point ``SnowflakeClient.from_env`` at a fresh FakeSnowflake-backed client.
|
|
25
|
+
|
|
26
|
+
Returns the fake so a test can register results before invoking the CLI.
|
|
27
|
+
"""
|
|
28
|
+
fake = FakeSnowflake()
|
|
29
|
+
|
|
30
|
+
def _from_env() -> SnowflakeClient:
|
|
31
|
+
return make_client(fake)
|
|
32
|
+
|
|
33
|
+
monkeypatch.setattr(SnowflakeClient, "from_env", staticmethod(_from_env))
|
|
34
|
+
return fake
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# build_parser / argv plumbing
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_build_parser_parses_query() -> None:
|
|
43
|
+
args = cli.build_parser().parse_args(["query", "SELECT 1"])
|
|
44
|
+
assert args.command == "query"
|
|
45
|
+
assert args.sql == "SELECT 1"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_version(capsys: pytest.CaptureFixture[str]) -> None:
|
|
49
|
+
# argparse's `version` action prints then raises SystemExit(0).
|
|
50
|
+
from snowflake_sql_api import __version__
|
|
51
|
+
|
|
52
|
+
with pytest.raises(SystemExit) as excinfo:
|
|
53
|
+
cli.main(["--version"])
|
|
54
|
+
assert excinfo.value.code == 0
|
|
55
|
+
assert __version__ in capsys.readouterr().out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_no_subcommand_prints_help_and_exits_2(
|
|
59
|
+
capsys: pytest.CaptureFixture[str],
|
|
60
|
+
) -> None:
|
|
61
|
+
assert cli.main([]) == 2
|
|
62
|
+
out = capsys.readouterr().out
|
|
63
|
+
assert "usage:" in out
|
|
64
|
+
assert "query" in out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# query command (stubbed client)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_query_happy_path_prints_json(
|
|
73
|
+
stub_from_env: FakeSnowflake, capsys: pytest.CaptureFixture[str]
|
|
74
|
+
) -> None:
|
|
75
|
+
stub_from_env.register(
|
|
76
|
+
"SELECT id, name FROM users",
|
|
77
|
+
[{"ID": 1, "NAME": "alice"}, {"ID": 2, "NAME": "bob"}],
|
|
78
|
+
)
|
|
79
|
+
assert cli.main(["query", "SELECT id, name FROM users"]) == 0
|
|
80
|
+
out = capsys.readouterr().out
|
|
81
|
+
assert json.loads(out) == [
|
|
82
|
+
{"ID": 1, "NAME": "alice"},
|
|
83
|
+
{"ID": 2, "NAME": "bob"},
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_query_serializes_non_json_native_values(
|
|
88
|
+
stub_from_env: FakeSnowflake, capsys: pytest.CaptureFixture[str]
|
|
89
|
+
) -> None:
|
|
90
|
+
# Decimal/timestamp/binary travel through _json_default on the way to stdout.
|
|
91
|
+
stub_from_env.register(
|
|
92
|
+
"SELECT amount, ts, blob",
|
|
93
|
+
[
|
|
94
|
+
{
|
|
95
|
+
"AMOUNT": Decimal("10.50"),
|
|
96
|
+
"TS": datetime(2024, 1, 2, 3, 4, 5, tzinfo=timezone.utc),
|
|
97
|
+
"BLOB": b"\x00\xff",
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
)
|
|
101
|
+
assert cli.main(["query", "SELECT amount, ts, blob"]) == 0
|
|
102
|
+
row = json.loads(capsys.readouterr().out)[0]
|
|
103
|
+
assert row["AMOUNT"] == "10.50"
|
|
104
|
+
assert row["TS"].startswith("2024-01-02T03:04:05")
|
|
105
|
+
assert row["BLOB"] == "00ff"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_query_snowflake_error_exits_1(
|
|
109
|
+
stub_from_env: FakeSnowflake, capsys: pytest.CaptureFixture[str]
|
|
110
|
+
) -> None:
|
|
111
|
+
stub_from_env.register_error(
|
|
112
|
+
"SELECT * FROM nope", "Object 'NOPE' does not exist", code="002003"
|
|
113
|
+
)
|
|
114
|
+
assert cli.main(["query", "SELECT * FROM nope"]) == 1
|
|
115
|
+
captured = capsys.readouterr()
|
|
116
|
+
assert captured.out == ""
|
|
117
|
+
assert captured.err.startswith("error: ")
|
|
118
|
+
assert "does not exist" in captured.err
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
# _json_default serializer (direct unit coverage, incl. the TypeError branch)
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@pytest.mark.parametrize(
|
|
127
|
+
("value", "expected"),
|
|
128
|
+
[
|
|
129
|
+
(Decimal("1.25"), "1.25"),
|
|
130
|
+
(datetime(2024, 6, 1, 12, 30, 0), "2024-06-01T12:30:00"),
|
|
131
|
+
(date(2024, 6, 1), "2024-06-01"),
|
|
132
|
+
(time(9, 15, 30), "09:15:30"),
|
|
133
|
+
(b"\xde\xad", "dead"),
|
|
134
|
+
(bytearray(b"\xbe\xef"), "beef"),
|
|
135
|
+
],
|
|
136
|
+
)
|
|
137
|
+
def test_json_default_serializes(value: object, expected: str) -> None:
|
|
138
|
+
assert cli._json_default(value) == expected
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_json_default_rejects_unsupported() -> None:
|
|
142
|
+
with pytest.raises(TypeError, match="cannot serialize"):
|
|
143
|
+
cli._json_default(object())
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Guard the install footprint: a bare ``import snowflake_sql_api`` stays light.
|
|
2
|
+
|
|
3
|
+
The package's whole value proposition is a small install and a fast cold start
|
|
4
|
+
(serverless / Lambda). Two regressions would silently erode that:
|
|
5
|
+
|
|
6
|
+
1. A heavy optional dependency (pandas / pyarrow) getting imported at module top
|
|
7
|
+
instead of behind its extra. The ``[pandas]`` feature must stay opt-in.
|
|
8
|
+
2. Import time ballooning for any reason.
|
|
9
|
+
|
|
10
|
+
Both checks run in a **fresh subprocess** so the parent pytest process's
|
|
11
|
+
already-imported modules can't mask a regression (pandas may well be imported by
|
|
12
|
+
the time these tests run). The leak check installs a recording meta-path finder
|
|
13
|
+
that stubs the forbidden names, so it fires deterministically whether or not the
|
|
14
|
+
real packages are installed: a module-top ``import pandas`` is caught even when
|
|
15
|
+
it is guarded by ``try/except ImportError`` and even in an env without pandas.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
# Heavy optional deps that a bare import must never pull in. pandas is the
|
|
24
|
+
# ``[pandas]`` extra; pyarrow/numpy ride in with it. If any is imported at module
|
|
25
|
+
# top, something bypassed the optional-import guard.
|
|
26
|
+
FORBIDDEN_ON_BARE_IMPORT = ("pandas", "pyarrow", "numpy")
|
|
27
|
+
|
|
28
|
+
# Generous ceiling: the real cold import (httpx + PyJWT + cryptography, all pure
|
|
29
|
+
# Python or fast C imports) is well under a second. This is a coarse backstop
|
|
30
|
+
# against an order-of-magnitude regression, not a precise benchmark, so the bound
|
|
31
|
+
# is deliberately loose and we assert on the best of several samples to stay
|
|
32
|
+
# non-flaky on shared CI runners.
|
|
33
|
+
IMPORT_BUDGET_SECONDS = 2.5
|
|
34
|
+
TIMING_SAMPLES = 3
|
|
35
|
+
|
|
36
|
+
# Subprocess: record any *attempt* to import a forbidden module during a bare
|
|
37
|
+
# `import snowflake_sql_api`. The recorder returns a stub module for forbidden
|
|
38
|
+
# names, so the import succeeds (and is recorded) regardless of whether the real
|
|
39
|
+
# dependency is installed -- making the guard deterministic across environments.
|
|
40
|
+
_LEAK_SCRIPT = """
|
|
41
|
+
import importlib.abc
|
|
42
|
+
import importlib.machinery
|
|
43
|
+
import sys
|
|
44
|
+
import types
|
|
45
|
+
|
|
46
|
+
FORBIDDEN = {forbidden!r}
|
|
47
|
+
attempted = []
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _Recorder(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
|
51
|
+
def find_spec(self, name, path, target=None):
|
|
52
|
+
if name.split(".")[0] in FORBIDDEN:
|
|
53
|
+
attempted.append(name.split(".")[0])
|
|
54
|
+
return importlib.machinery.ModuleSpec(name, self)
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def create_module(self, spec):
|
|
58
|
+
return types.ModuleType(spec.name)
|
|
59
|
+
|
|
60
|
+
def exec_module(self, module):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
sys.meta_path.insert(0, _Recorder())
|
|
65
|
+
import snowflake_sql_api # noqa: F401
|
|
66
|
+
print(",".join(sorted(set(attempted))))
|
|
67
|
+
""".format(
|
|
68
|
+
forbidden=FORBIDDEN_ON_BARE_IMPORT
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
_TIMING_SCRIPT = """
|
|
72
|
+
import time
|
|
73
|
+
start = time.perf_counter()
|
|
74
|
+
import snowflake_sql_api # noqa: F401
|
|
75
|
+
print(time.perf_counter() - start)
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _run(script: str) -> subprocess.CompletedProcess:
|
|
80
|
+
return subprocess.run(
|
|
81
|
+
[sys.executable, "-c", script], capture_output=True, text=True
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_bare_import_pulls_no_heavy_deps() -> None:
|
|
86
|
+
result = _run(_LEAK_SCRIPT)
|
|
87
|
+
assert (
|
|
88
|
+
result.returncode == 0
|
|
89
|
+
), f"`import snowflake_sql_api` failed in a clean subprocess:\n{result.stderr}"
|
|
90
|
+
attempted = [name for name in result.stdout.strip().split(",") if name]
|
|
91
|
+
assert not attempted, (
|
|
92
|
+
f"bare `import snowflake_sql_api` tried to import heavy deps: {attempted}. "
|
|
93
|
+
"Keep optional dependencies behind their extra (lazy import)."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_bare_import_is_fast() -> None:
|
|
98
|
+
samples = []
|
|
99
|
+
for _ in range(TIMING_SAMPLES):
|
|
100
|
+
result = _run(_TIMING_SCRIPT)
|
|
101
|
+
assert result.returncode == 0, result.stderr
|
|
102
|
+
samples.append(float(result.stdout.strip()))
|
|
103
|
+
best = min(samples)
|
|
104
|
+
assert best < IMPORT_BUDGET_SECONDS, (
|
|
105
|
+
f"fastest of {TIMING_SAMPLES} imports was {best:.3f}s, over the "
|
|
106
|
+
f"{IMPORT_BUDGET_SECONDS}s budget. A heavy import likely crept into the "
|
|
107
|
+
"module-load path."
|
|
108
|
+
)
|
|
@@ -46,6 +46,7 @@ def test_make_client_returns_real_client() -> None:
|
|
|
46
46
|
assert isinstance(client, SnowflakeClient)
|
|
47
47
|
assert client.query_scalar("SELECT 1") == 1
|
|
48
48
|
client.close()
|
|
49
|
+
assert client._transport._client.is_closed
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
def test_make_client_rejects_managed_kwargs() -> None:
|
|
@@ -304,6 +305,7 @@ async def test_async_query() -> None:
|
|
|
304
305
|
assert isinstance(client, AsyncSnowflakeClient)
|
|
305
306
|
assert await client.query("SELECT id FROM t") == [{"ID": 1}, {"ID": 2}]
|
|
306
307
|
await client.aclose()
|
|
308
|
+
assert client._transport._client.is_closed
|
|
307
309
|
|
|
308
310
|
|
|
309
311
|
async def test_async_submit_and_result() -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|