Skip to content

Commit 3e547e8

Browse files
committed
Move psycopg param generator to DAB utility
1 parent 246f96a commit 3e547e8

File tree

2 files changed

+121
-1
lines changed

2 files changed

+121
-1
lines changed

ansible_base/lib/utils/db.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
from contextlib import contextmanager
2+
from copy import deepcopy
3+
from typing import Union
24
from zlib import crc32
35

6+
from django.conf import settings
47
from django.db import DEFAULT_DB_ALIAS, connection, connections, transaction
8+
from django.db.backends.postgresql.base import DatabaseWrapper as PsycopgDatabaseWrapper
59
from django.db.migrations.executor import MigrationExecutor
610

711

@@ -149,3 +153,51 @@ def advisory_lock(*args, lock_session_timeout_milliseconds=0, **kwargs):
149153
yield True
150154
else:
151155
raise RuntimeError(f'Advisory lock not implemented for database type {connection.vendor}')
156+
157+
158+
settings_dict_type = dict[str, Union[str, int]]
159+
160+
161+
def psycopg_kwargs_from_settings_dict(settings_dict: settings_dict_type) -> dict:
162+
"""Return psycopg connection creation kwargs given Django db settings info
163+
164+
:param dict setting_dict: DATABASES in Django settings
165+
:return: kwargs that can be passed to psycopg.connect, or connection classes"""
166+
return PsycopgDatabaseWrapper(settings_dict).get_connection_params()
167+
168+
169+
def combine_settings_dict(
170+
settings_dict1: dict[str, Union[str, int]], settings_dict2: dict[str, Union[str, int]], **extra_options
171+
) -> dict[str, Union[str, int]]:
172+
"""Given two Django settings dictionaries, combine them and return a new settings_dict"""
173+
settings_dict = deepcopy(settings_dict1)
174+
settings_dict['OPTIONS'] = deepcopy(settings_dict.get('OPTIONS', {}))
175+
176+
# These extra options are used by AWX to set application_name
177+
settings_dict['OPTIONS'].update(extra_options)
178+
179+
# Apply overrides specifically for the listener connection
180+
for k, v in settings_dict2.items():
181+
if k != 'OPTIONS':
182+
settings_dict[k] = v
183+
184+
for k, v in settings_dict2.get('OPTIONS', {}).items():
185+
settings_dict['OPTIONS'][k] = v
186+
187+
return settings_dict
188+
189+
190+
def get_pg_notify_params(alias: str = DEFAULT_DB_ALIAS, **extra_options) -> dict:
191+
pg_notify_overrides = {}
192+
if hasattr(settings, 'PG_NOTIFY_DATABASES'):
193+
pg_notify_overrides = settings.PG_NOTIFY_DATABASES.get(alias, {})
194+
elif hasattr(settings, 'LISTENER_DATABASES'):
195+
pg_notify_overrides = settings.LISTENER_DATABASES.get(alias, {})
196+
197+
settings_dict = combine_settings_dict(settings.DATABASES[alias], pg_notify_overrides, **extra_options)
198+
199+
# Reuse the Django postgres DB backend to create params for the psycopg library
200+
psycopg_params = psycopg_kwargs_from_settings_dict(settings_dict)
201+
psycopg_params['autocommit'] = True
202+
203+
return psycopg_params

test_app/tests/lib/utils/test_db.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import pytest
55
from django.db import connection
66
from django.db.utils import OperationalError
7+
from django.test import override_settings
78

8-
from ansible_base.lib.utils.db import advisory_lock, migrations_are_complete
9+
from ansible_base.lib.utils.db import advisory_lock, get_pg_notify_params, migrations_are_complete, psycopg_kwargs_from_settings_dict
910

1011

1112
@pytest.mark.django_db
@@ -14,6 +15,73 @@ def test_migrations_are_complete():
1415
assert migrations_are_complete()
1516

1617

18+
class TestPGNotifyConnection:
19+
TEST_DATABASE_DICT = {
20+
"default": {
21+
"ENGINE": "django.db.backends.postgresql",
22+
"HOST": "https://foo.invalid",
23+
"PORT": 55434,
24+
"USER": "dab_user",
25+
"PASSWORD": "dabbing",
26+
"NAME": "dab_db",
27+
}
28+
}
29+
PSYCOPG_KWARGS = {
30+
'dbname': 'dab_db',
31+
'client_encoding': 'UTF8',
32+
# kwargs containing objects can not be compared so they will be ignored
33+
# 'cursor_factory': <class 'django.db.backends.postgresql.base.Cursor'>,
34+
'user': 'dab_user',
35+
'password': 'dabbing',
36+
'host': 'https://foo.invalid',
37+
'port': 55434,
38+
# 'context': <psycopg.adapt.AdaptersMap object at 0x7f537f2d9f70>,
39+
'prepare_threshold': None,
40+
'autocommit': True,
41+
}
42+
43+
@pytest.fixture
44+
def mock_settings(self):
45+
with override_settings(DATABASES=self.TEST_DATABASE_DICT, USE_TZ=False):
46+
yield
47+
48+
def _trim_python_objects(self, psycopg_params):
49+
# These remove those commented-out kwargs in PSYCOPG_KWARGS
50+
psycopg_params.pop('cursor_factory')
51+
psycopg_params.pop('context')
52+
return psycopg_params
53+
54+
def test_default_behavior(self, mock_settings):
55+
params = self._trim_python_objects(get_pg_notify_params())
56+
assert params == self.PSYCOPG_KWARGS
57+
58+
def test_pg_notify_extra_options(self, mock_settings):
59+
params = self._trim_python_objects(get_pg_notify_params(application_name='joe_connection'))
60+
expected = self.PSYCOPG_KWARGS.copy()
61+
expected['application_name'] = 'joe_connection'
62+
assert params == expected
63+
64+
def test_lister_databases(self, mock_settings):
65+
LISTENER_DATABASES = {"default": {"HOST": "https://foo.anotherhost.invalid"}}
66+
with override_settings(LISTENER_DATABASES=LISTENER_DATABASES):
67+
params = self._trim_python_objects(get_pg_notify_params())
68+
assert params['host'] == "https://foo.anotherhost.invalid"
69+
70+
def test_pg_notify_databases(self, mock_settings):
71+
PG_NOTIFY_DATABASES = {"default": {"HOST": "https://foo.anotherhost2.invalid"}}
72+
with override_settings(PG_NOTIFY_DATABASES=PG_NOTIFY_DATABASES):
73+
params = self._trim_python_objects(get_pg_notify_params())
74+
assert params['host'] == "https://foo.anotherhost2.invalid"
75+
76+
def test_psycopg_kwargs_from_settings_dict(self):
77+
"More of a unit test, doing the same thing"
78+
test_dict = self.TEST_DATABASE_DICT["default"].copy()
79+
test_dict['OPTIONS'] = {'autocommit': True}
80+
with override_settings(USE_TZ=False):
81+
psycopg_params = self._trim_python_objects(psycopg_kwargs_from_settings_dict(test_dict))
82+
assert psycopg_params == self.PSYCOPG_KWARGS
83+
84+
1785
class TestAdvisoryLock:
1886
@pytest.fixture(autouse=True)
1987
def skip_if_sqlite(self):

0 commit comments

Comments
 (0)