Skip to content

Commit 8c5f9c5

Browse files
Merge branch 'main' into qa
2 parents 8e29458 + 0cf21cc commit 8c5f9c5

4 files changed

Lines changed: 74 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## v1.0.1 - 4/3/23
4+
- Add transaction support to RedshiftClient
5+
36
## v1.0.0 - 3/22/23
47
- Improve Oauth2ApiClient token refresh and method responses
58
- Create separate PostgreSQLClient and PostgreSQLPoolClient classes

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "nypl_py_utils"
7-
version = "1.0.0"
7+
version = "1.0.1"
88
authors = [
99
{ name="Aaron Friedman", email="aaronfriedman@nypl.org" },
1010
]

src/nypl_py_utils/classes/redshift_client.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,39 @@ def execute_query(self, query, dataframe=False):
7272
finally:
7373
cursor.close()
7474

75+
def execute_transaction(self, queries):
76+
"""
77+
Executes a series of queries within a single transaction against the
78+
given database connection. Assumes each of these queries is a write
79+
query and so does not return anything.
80+
81+
Parameters
82+
----------
83+
queries: list<tuple>
84+
A list of tuples containing a query and the values to be used if
85+
the query is parameterized (or None if it's not)
86+
"""
87+
self.logger.info('Executing transaction against {} database'.format(
88+
self.database))
89+
try:
90+
cursor = self.conn.cursor()
91+
cursor.execute('BEGIN TRANSACTION;')
92+
for query in queries:
93+
self.logger.debug('Executing query {}'.format(query))
94+
cursor.execute(query[0], query[1])
95+
cursor.execute('END TRANSACTION;')
96+
self.conn.commit()
97+
except Exception as e:
98+
self.conn.rollback()
99+
self.logger.error(
100+
('Error executing {name} database transaction: {error}')
101+
.format(name=self.database, error=e))
102+
raise RedshiftClientError(
103+
('Error executing {name} database transaction: {error}')
104+
.format(name=self.database, error=e)) from None
105+
finally:
106+
cursor.close()
107+
75108
def close_connection(self):
76109
"""Closes the database connection"""
77110
self.logger.debug('Closing {} database connection'.format(

tests/test_redshift_client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,43 @@ def test_execute_query_with_exception(
6262
test_instance.conn.rollback.assert_called_once()
6363
mock_cursor.close.assert_called_once()
6464

65+
def test_execute_transaction(self, mock_redshift_conn, test_instance,
66+
mocker):
67+
test_instance.connect()
68+
69+
mock_cursor = mocker.MagicMock()
70+
test_instance.conn.cursor.return_value = mock_cursor
71+
72+
test_instance.execute_transaction([('query 1', None),
73+
('query 2 %s %s', ('a', 1))])
74+
mock_cursor.execute.assert_has_calls([
75+
mocker.call('BEGIN TRANSACTION;'),
76+
mocker.call('query 1', None),
77+
mocker.call('query 2 %s %s', ('a', 1)),
78+
mocker.call('END TRANSACTION;')])
79+
test_instance.conn.commit.assert_called_once()
80+
mock_cursor.close.assert_called_once()
81+
82+
def test_execute_transaction_with_exception(
83+
self, mock_redshift_conn, test_instance, mocker):
84+
test_instance.connect()
85+
86+
mock_cursor = mocker.MagicMock()
87+
mock_cursor.execute.side_effect = [None, None, Exception()]
88+
test_instance.conn.cursor.return_value = mock_cursor
89+
90+
with pytest.raises(RedshiftClientError):
91+
test_instance.execute_transaction(
92+
[('query 1', None), ('query 2', None)])
93+
94+
mock_cursor.execute.assert_has_calls([
95+
mocker.call('BEGIN TRANSACTION;'),
96+
mocker.call('query 1', None),
97+
mocker.call('query 2', None)])
98+
test_instance.conn.commit.assert_not_called()
99+
test_instance.conn.rollback.assert_called_once()
100+
mock_cursor.close.assert_called_once()
101+
65102
def test_close_connection(self, mock_redshift_conn, test_instance):
66103
test_instance.connect()
67104
test_instance.close_connection()

0 commit comments

Comments
 (0)