Skip to content

Commit 20a0296

Browse files
Merge pull request #61 from NYPL/snowflake-connect
Allow connecting to snowflake with any parameters
2 parents 3a384df + 98e648a commit 20a0296

4 files changed

Lines changed: 62 additions & 117 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# Changelog
2+
## v1.11.0 4/27/26
3+
- Allow Snowflake client to connect using any parameters
4+
25
## v1.10.2 4/27/26
36
- Prevent double logging in pytest using structlog
47

pyproject.toml

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "nypl_py_utils"
7-
version = "1.10.2"
7+
version = "1.11.0"
88
authors = [
99
{ name="Aaron Friedman", email="aaronfriedman@nypl.org" },
1010
]
1111
description = "A package containing Python utilities for use across NYPL"
1212
readme = "README.md"
13-
requires-python = ">=3.9"
13+
requires-python = ">=3.10"
1414
classifiers = [
1515
"Programming Language :: Python :: 3",
1616
"License :: OSI Approved :: MIT License",
@@ -25,85 +25,85 @@ dependencies = []
2525
[project.optional-dependencies]
2626
avro-client = [
2727
"nypl_py_utils[log-helper]",
28-
"fastavro>=1.11.1",
29-
"requests>=2.28.1"
28+
"fastavro==1.12.2",
29+
"requests==2.33.1"
3030
]
3131
cloudlibrary-client = [
3232
"nypl_py_utils[log-helper]",
33-
"requests>=2.28.1"
33+
"requests==2.33.1"
3434
]
3535
kinesis-client = [
3636
"nypl_py_utils[log-helper]",
37-
"boto3>=1.26.5",
38-
"botocore>=1.29.5"
37+
"boto3==1.43.1",
38+
"botocore==1.43.1"
3939
]
4040
kms-client = [
4141
"nypl_py_utils[log-helper]",
42-
"boto3>=1.26.5",
43-
"botocore>=1.29.5"
42+
"boto3==1.43.1",
43+
"botocore==1.43.1"
4444
]
4545
mysql-client = [
4646
"nypl_py_utils[log-helper]",
47-
"mysql-connector-python>=8.0.32"
47+
"mysql-connector-python==9.7.0"
4848
]
4949
oauth2-api-client = [
5050
"nypl_py_utils[log-helper]",
51-
"oauthlib>=3.2.2",
52-
"requests_oauthlib>=1.3.1"
51+
"oauthlib==3.3.1",
52+
"requests_oauthlib==2.0.0"
5353
]
5454
postgresql-client = [
5555
"nypl_py_utils[log-helper]",
56-
"psycopg[binary]>=3.1.6"
56+
"psycopg[binary]==3.3.3"
5757
]
5858
redshift-client = [
5959
"nypl_py_utils[log-helper]",
60-
"botocore>=1.29.5",
61-
"redshift-connector>=2.0.909"
60+
"botocore==1.43.1",
61+
"redshift-connector==2.1.13"
6262
]
6363
s3-client = [
6464
"nypl_py_utils[log-helper]",
65-
"boto3>=1.26.5",
66-
"botocore>=1.29.5"
65+
"boto3==1.43.1",
66+
"botocore==1.43.1"
6767
]
6868
secrets-manager-client = [
6969
"nypl_py_utils[log-helper]",
70-
"boto3>=1.26.5",
71-
"botocore>=1.29.5"
70+
"boto3==1.43.1",
71+
"botocore==1.43.1"
7272
]
7373
sftp-client = [
7474
"nypl_py_utils[log-helper]",
75-
"paramiko>=3.4.1"
75+
"paramiko==4.0.0"
7676
]
7777
snowflake-client = [
7878
"nypl_py_utils[log-helper]",
79-
"snowflake-connector-python>=4.3.0"
79+
"snowflake-connector-python==4.3.0"
8080
]
8181
config-helper = [
8282
"nypl_py_utils[kms-client,log-helper]",
83-
"PyYAML>=6.0"
83+
"PyYAML==6.0.3"
8484
]
8585
log-helper = [
86-
"structlog>=25.5.0"
86+
"structlog==25.5.0"
8787
]
8888
obfuscation-helper = [
8989
"nypl_py_utils[log-helper]",
90-
"bcrypt>=4.0.1"
90+
"bcrypt==5.0.0"
9191
]
9292
patron-data-helper = [
93-
"nypl_py_utils[postgresql-client,redshift-client,log-helper]>=1.1.5",
94-
"pandas>=2.2.2"
93+
"nypl_py_utils[postgresql-client,redshift-client,log-helper]",
94+
"pandas==3.0.2"
9595
]
9696
research-catalog-identifier-helper = [
97-
"requests>=2.28.1"
97+
"requests==2.33.1"
9898
]
9999
development = [
100100
"nypl_py_utils[avro-client,cloudlibrary-client,kinesis-client,kms-client,mysql-client,oauth2-api-client,postgresql-client,redshift-client,s3-client,secrets-manager-client,sftp-client,snowflake-client,config-helper,log-helper,obfuscation-helper,patron-data-helper,research-catalog-identifier-helper]",
101-
"flake8>=6.0.0",
102-
"freezegun>=1.2.2",
103-
"mock>=4.0.3",
104-
"pytest>=8.0.0",
105-
"pytest-mock>=3.10.0",
106-
"requests-mock>=1.10.0"
101+
"flake8==7.3.0",
102+
"freezegun==1.5.5",
103+
"mock==5.2.0",
104+
"pytest==9.0.3",
105+
"pytest-mock==3.15.1",
106+
"requests-mock==1.12.1"
107107
]
108108

109109
[tool.pytest.ini_options]

src/nypl_py_utils/classes/snowflake_client.py

Lines changed: 22 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,67 +6,34 @@
66
class SnowflakeClient:
77
"""Client for managing connections to Snowflake"""
88

9-
def __init__(self, account, user, private_key=None, password=None):
9+
def __init__(self, connection_params):
10+
"""
11+
See the `connect` method below for what `connection_params` should
12+
look like
13+
"""
1014
self.logger = create_log('snowflake_client')
11-
if (password is None) == (private_key is None):
12-
raise SnowflakeClientError(
13-
'Either password or private key must be set (but not both)',
14-
self.logger
15-
) from None
16-
15+
self.connection_params = connection_params
1716
self.conn = None
18-
self.account = account
19-
self.user = user
20-
self.private_key = private_key
21-
self.password = password
2217

23-
def connect(self, mfa_code=None, **kwargs):
18+
def connect(self):
2419
"""
25-
Connects to Snowflake using the given credentials. If you're connecting
26-
locally, you should be using the password and mfa_code. If the
27-
connection is for production code, a private_key should be set up.
28-
29-
Parameters
30-
----------
31-
mfa_code: str, optional
32-
The six-digit MFA code. Only necessary for connecting as a human
33-
user.
34-
kwargs:
35-
All possible arguments (such as which warehouse to use or how
36-
long to wait before timing out) can be found here:
37-
https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#connect
20+
Connects to Snowflake using the given connection parameters. In
21+
practice, there are likely two sets of parameters that will be used:
22+
1. Local development: `connection_name` and `private_key_file_pwd`.
23+
This method requires a `connections.toml` file with a matching
24+
`connection_name` connection.
25+
2. Production code: `account`, `user`, and `private_key`.
26+
27+
All possible parameters can be found here:
28+
https://docs.snowflake.com/en/developer-guide/python-connector/python-connector-api#connect
3829
"""
3930
self.logger.info('Connecting to Snowflake')
40-
if self.private_key is not None:
41-
try:
42-
self.conn = sc.connect(
43-
account=self.account,
44-
user=self.user,
45-
private_key=self.private_key,
46-
**kwargs)
47-
except Exception as e:
48-
raise SnowflakeClientError(
49-
f'Error connecting to Snowflake: {e}', self.logger
50-
) from None
51-
else:
52-
if mfa_code is None:
53-
raise SnowflakeClientError(
54-
'When using a password, an MFA code must also be provided',
55-
self.logger
56-
) from None
57-
58-
pw = self.password + mfa_code
59-
try:
60-
self.conn = sc.connect(
61-
account=self.account,
62-
user=self.user,
63-
password=pw,
64-
passcode_in_password=True,
65-
**kwargs)
66-
except Exception as e:
67-
raise SnowflakeClientError(
68-
f'Error connecting to Snowflake: {e}', self.logger
69-
) from None
31+
try:
32+
self.conn = sc.connect(**self.connection_params)
33+
except Exception as e:
34+
raise SnowflakeClientError(
35+
f'Error connecting to Snowflake: {e}', self.logger
36+
) from None
7037

7138
def execute_query(self, query, **kwargs):
7239
"""

tests/test_snowflake_client.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,13 @@ def mock_snowflake_conn(self, mocker):
1414
@pytest.fixture
1515
def test_instance(self):
1616
return SnowflakeClient(
17-
'test_account', 'test_user', private_key='test_pk')
17+
{'account': 'test_account', 'user': 'test_user'}
18+
)
1819

19-
def test_init_no_pw(self):
20-
with pytest.raises(SnowflakeClientError):
21-
SnowflakeClient('test_account', 'test_user')
22-
23-
def test_init_multiple_auth(self):
24-
with pytest.raises(SnowflakeClientError):
25-
SnowflakeClient('test_account', 'test_user', 'test_pk', 'test_pw')
26-
27-
def test_connect_with_pk(self, mock_snowflake_conn, test_instance):
20+
def test_connect(self, mock_snowflake_conn, test_instance):
2821
test_instance.connect()
2922
mock_snowflake_conn.assert_called_once_with(
30-
account='test_account',
31-
user='test_user',
32-
private_key='test_pk')
33-
34-
def test_connect_with_pw(self, mock_snowflake_conn):
35-
test_instance = SnowflakeClient(
36-
'test_account', 'test_user', password='test_pw')
37-
test_instance.connect('123456')
38-
mock_snowflake_conn.assert_called_once_with(
39-
account='test_account',
40-
user='test_user',
41-
password='test_pw123456',
42-
passcode_in_password=True)
43-
44-
def test_connect_no_mfa(self, mock_snowflake_conn):
45-
test_instance = SnowflakeClient(
46-
'test_account', 'test_user', password='test_pw')
47-
with pytest.raises(SnowflakeClientError):
48-
test_instance.connect()
23+
account='test_account', user='test_user')
4924

5025
def test_execute_query(
5126
self, mock_snowflake_conn, test_instance, mocker):

0 commit comments

Comments
 (0)