Skip to content

Commit 4753e3f

Browse files
committed
add support for psycopg3
1 parent 2976b25 commit 4753e3f

File tree

7 files changed

+272
-0
lines changed

7 files changed

+272
-0
lines changed

aws_xray_sdk/core/patcher.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
'pymongo',
2424
'pymysql',
2525
'psycopg2',
26+
'psycopg',
2627
'pg8000',
2728
'sqlalchemy_core',
2829
'httpx',
@@ -38,6 +39,7 @@
3839
'pymongo',
3940
'pymysql',
4041
'psycopg2',
42+
'psycopg',
4143
'pg8000',
4244
'sqlalchemy_core',
4345
'httpx',

aws_xray_sdk/ext/psycopg/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .patch import patch
2+
3+
4+
__all__ = ['patch']

aws_xray_sdk/ext/psycopg/patch.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import copy
2+
import re
3+
import wrapt
4+
from operator import methodcaller
5+
6+
from aws_xray_sdk.ext.dbapi2 import XRayTracedConn, XRayTracedCursor
7+
8+
9+
def patch():
10+
wrapt.wrap_function_wrapper(
11+
'psycopg',
12+
'connect',
13+
_xray_traced_connect
14+
)
15+
wrapt.wrap_function_wrapper(
16+
'psycopg.extensions',
17+
'register_type',
18+
_xray_register_type_fix
19+
)
20+
wrapt.wrap_function_wrapper(
21+
'psycopg.extensions',
22+
'quote_ident',
23+
_xray_register_type_fix
24+
)
25+
26+
wrapt.wrap_function_wrapper(
27+
'psycopg.extras',
28+
'register_default_jsonb',
29+
_xray_register_default_jsonb_fix
30+
)
31+
32+
33+
def _xray_traced_connect(wrapped, instance, args, kwargs):
34+
conn = wrapped(*args, **kwargs)
35+
parameterized_dsn = {c[0]: c[-1] for c in map(methodcaller('split', '='), conn.dsn.split(' '))}
36+
meta = {
37+
'database_type': 'PostgreSQL',
38+
'url': 'postgresql://{}@{}:{}/{}'.format(
39+
parameterized_dsn.get('user', 'unknown'),
40+
parameterized_dsn.get('host', 'unknown'),
41+
parameterized_dsn.get('port', 'unknown'),
42+
parameterized_dsn.get('dbname', 'unknown'),
43+
),
44+
'user': parameterized_dsn.get('user', 'unknown'),
45+
'database_version': str(conn.server_version),
46+
'driver_version': 'Psycopg 3'
47+
}
48+
49+
return XRayTracedConn(conn, meta)
50+
51+
52+
def _xray_register_type_fix(wrapped, instance, args, kwargs):
53+
"""Send the actual connection or curser to register type."""
54+
our_args = list(copy.copy(args))
55+
if len(our_args) == 2 and isinstance(our_args[1], (XRayTracedConn, XRayTracedCursor)):
56+
our_args[1] = our_args[1].__wrapped__
57+
58+
return wrapped(*our_args, **kwargs)
59+
60+
61+
def _xray_register_default_jsonb_fix(wrapped, instance, args, kwargs):
62+
our_kwargs = dict()
63+
for key, value in kwargs.items():
64+
if key == "conn_or_curs" and isinstance(value, (XRayTracedConn, XRayTracedCursor)):
65+
# unwrap the connection or cursor to be sent to register_default_jsonb
66+
value = value.__wrapped__
67+
our_kwargs[key] = value
68+
69+
return wrapped(*args, **our_kwargs)

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Currently supported web frameworks and libraries:
2929
* mysql-connector
3030
* pg8000
3131
* psycopg2
32+
* psycopg (psycopg3)
3233
* pymongo
3334
* pymysql
3435
* pynamodb

tests/ext/psycopg/__init__.py

Whitespace-only changes.

tests/ext/psycopg/test_psycopg.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import psycopg
2+
import psycopg.extras
3+
import psycopg.pool
4+
import psycopg.sql
5+
6+
import pytest
7+
import testing.postgresql
8+
9+
from aws_xray_sdk.core import patch
10+
from aws_xray_sdk.core import xray_recorder
11+
from aws_xray_sdk.core.context import Context
12+
13+
patch(('psycopg',))
14+
15+
16+
@pytest.fixture(autouse=True)
17+
def construct_ctx():
18+
"""
19+
Clean up context storage on each test run and begin a segment
20+
so that later subsegment can be attached. After each test run
21+
it cleans up context storage again.
22+
"""
23+
xray_recorder.configure(service='test', sampling=False, context=Context())
24+
xray_recorder.clear_trace_entities()
25+
xray_recorder.begin_segment('name')
26+
yield
27+
xray_recorder.clear_trace_entities()
28+
29+
30+
def test_execute_dsn_kwargs():
31+
q = 'SELECT 1'
32+
with testing.postgresql.Postgresql() as postgresql:
33+
url = postgresql.url()
34+
dsn = postgresql.dsn()
35+
conn = psycopg.connect(dbname=dsn['database'],
36+
user=dsn['user'],
37+
password='',
38+
host=dsn['host'],
39+
port=dsn['port'])
40+
cur = conn.cursor()
41+
cur.execute(q)
42+
43+
subsegment = xray_recorder.current_segment().subsegments[0]
44+
assert subsegment.name == 'execute'
45+
sql = subsegment.sql
46+
assert sql['database_type'] == 'PostgreSQL'
47+
assert sql['user'] == dsn['user']
48+
assert sql['url'] == url
49+
assert sql['database_version']
50+
51+
52+
def test_execute_dsn_kwargs_alt_dbname():
53+
"""
54+
Psycopg supports database to be passed as `database` or `dbname`
55+
"""
56+
q = 'SELECT 1'
57+
58+
with testing.postgresql.Postgresql() as postgresql:
59+
url = postgresql.url()
60+
dsn = postgresql.dsn()
61+
conn = psycopg.connect(database=dsn['database'],
62+
user=dsn['user'],
63+
password='',
64+
host=dsn['host'],
65+
port=dsn['port'])
66+
cur = conn.cursor()
67+
cur.execute(q)
68+
69+
subsegment = xray_recorder.current_segment().subsegments[0]
70+
assert subsegment.name == 'execute'
71+
sql = subsegment.sql
72+
assert sql['database_type'] == 'PostgreSQL'
73+
assert sql['user'] == dsn['user']
74+
assert sql['url'] == url
75+
assert sql['database_version']
76+
77+
78+
def test_execute_dsn_string():
79+
q = 'SELECT 1'
80+
with testing.postgresql.Postgresql() as postgresql:
81+
url = postgresql.url()
82+
dsn = postgresql.dsn()
83+
conn = psycopg.connect('dbname=' + dsn['database'] +
84+
' password=mypassword' +
85+
' host=' + dsn['host'] +
86+
' port=' + str(dsn['port']) +
87+
' user=' + dsn['user'])
88+
cur = conn.cursor()
89+
cur.execute(q)
90+
91+
subsegment = xray_recorder.current_segment().subsegments[0]
92+
assert subsegment.name == 'execute'
93+
sql = subsegment.sql
94+
assert sql['database_type'] == 'PostgreSQL'
95+
assert sql['user'] == dsn['user']
96+
assert sql['url'] == url
97+
assert sql['database_version']
98+
99+
100+
def test_execute_in_pool():
101+
q = 'SELECT 1'
102+
with testing.postgresql.Postgresql() as postgresql:
103+
url = postgresql.url()
104+
dsn = postgresql.dsn()
105+
pool = psycopg.pool.SimpleConnectionPool(1, 1,
106+
dbname=dsn['database'],
107+
user=dsn['user'],
108+
password='',
109+
host=dsn['host'],
110+
port=dsn['port'])
111+
cur = pool.getconn(key=dsn['user']).cursor()
112+
cur.execute(q)
113+
114+
subsegment = xray_recorder.current_segment().subsegments[0]
115+
assert subsegment.name == 'execute'
116+
sql = subsegment.sql
117+
assert sql['database_type'] == 'PostgreSQL'
118+
assert sql['user'] == dsn['user']
119+
assert sql['url'] == url
120+
assert sql['database_version']
121+
122+
123+
def test_execute_bad_query():
124+
q = 'SELECT blarg'
125+
with testing.postgresql.Postgresql() as postgresql:
126+
url = postgresql.url()
127+
dsn = postgresql.dsn()
128+
conn = psycopg.connect(dbname=dsn['database'],
129+
user=dsn['user'],
130+
password='',
131+
host=dsn['host'],
132+
port=dsn['port'])
133+
cur = conn.cursor()
134+
try:
135+
cur.execute(q)
136+
except Exception:
137+
pass
138+
139+
subsegment = xray_recorder.current_segment().subsegments[0]
140+
assert subsegment.name == 'execute'
141+
sql = subsegment.sql
142+
assert sql['database_type'] == 'PostgreSQL'
143+
assert sql['user'] == dsn['user']
144+
assert sql['url'] == url
145+
assert sql['database_version']
146+
147+
exception = subsegment.cause['exceptions'][0]
148+
assert exception.type == 'UndefinedColumn'
149+
150+
151+
def test_register_extensions():
152+
with testing.postgresql.Postgresql() as postgresql:
153+
url = postgresql.url()
154+
dsn = postgresql.dsn()
155+
conn = psycopg.connect('dbname=' + dsn['database'] +
156+
' password=mypassword' +
157+
' host=' + dsn['host'] +
158+
' port=' + str(dsn['port']) +
159+
' user=' + dsn['user'])
160+
assert psycopg.extras.register_uuid(None, conn)
161+
assert psycopg.extras.register_uuid(None, conn.cursor())
162+
163+
164+
def test_query_as_string():
165+
with testing.postgresql.Postgresql() as postgresql:
166+
url = postgresql.url()
167+
dsn = postgresql.dsn()
168+
conn = psycopg.connect('dbname=' + dsn['database'] +
169+
' password=mypassword' +
170+
' host=' + dsn['host'] +
171+
' port=' + str(dsn['port']) +
172+
' user=' + dsn['user'])
173+
test_sql = psycopg.sql.Identifier('test')
174+
assert test_sql.as_string(conn)
175+
assert test_sql.as_string(conn.cursor())
176+
177+
178+
def test_register_default_jsonb():
179+
with testing.postgresql.Postgresql() as postgresql:
180+
url = postgresql.url()
181+
dsn = postgresql.dsn()
182+
conn = psycopg.connect('dbname=' + dsn['database'] +
183+
' password=mypassword' +
184+
' host=' + dsn['host'] +
185+
' port=' + str(dsn['port']) +
186+
' user=' + dsn['user'])
187+
188+
assert psycopg.extras.register_default_jsonb(conn_or_curs=conn, loads=lambda x: x)
189+
assert psycopg.extras.register_default_jsonb(conn_or_curs=conn.cursor(), loads=lambda x: x)

tox.ini

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ envlist =
3030

3131
py{37,38,39,310,311}-ext-psycopg2
3232

33+
py{37,38,39,310,311}-ext-psycopg
34+
3335
py{37,38,39,310,311}-ext-pymysql
3436

3537
py{37,38,39,310,311}-ext-pynamodb
@@ -94,6 +96,9 @@ deps =
9496
ext-psycopg2: psycopg2
9597
ext-psycopg2: testing.postgresql
9698

99+
ext-psycopg: psycopg
100+
ext-psycopg: testing.postgresql
101+
97102
ext-pg8000: pg8000 <= 1.20.0
98103
ext-pg8000: testing.postgresql
99104

@@ -130,6 +135,8 @@ commands =
130135
ext-pg8000: coverage run --append --source aws_xray_sdk -m pytest tests/ext/pg8000 {posargs}
131136

132137
ext-psycopg2: coverage run --append --source aws_xray_sdk -m pytest tests/ext/psycopg2 {posargs}
138+
139+
ext-psycopg: coverage run --append --source aws_xray_sdk -m pytest tests/ext/psycopg {posargs}
133140

134141
ext-pymysql: coverage run --append --source aws_xray_sdk -m pytest tests/ext/pymysql {posargs}
135142

0 commit comments

Comments
 (0)