Skip to content

Commit 3eaf4aa

Browse files
author
James William Pye
committed
Clean-up some delimiter escaping issues.
Simplify parsing by fulling parsing the RI without processing the fields. The sub-fields are then properly processed as needed. Add the ability to state driver parameters by specifying a setting in square brackets. For instance: "pq://host/db?[driver_param]=value". This provides a means for users to state and clearly identify parameters intended for the driver(notably, the server_encoding hint). Properly escape delimiters in sub-fields when serializing the string.
1 parent 8acded9 commit 3eaf4aa

1 file changed

Lines changed: 76 additions & 40 deletions

File tree

postgresql/iri.py

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,72 +7,108 @@
77
88
PQ IRIs take the form::
99
10-
pq://user:pass@host:port/database?setting=value#public,othernamespace
11-
12-
Liking to ``https``, the sslmode can be set to "require" by::
13-
14-
pqs://user:pass@host:port/database?setting=value#public,othernamespace
10+
pq://user:pass@host:port/database?setting=value&setting2=value2#public,othernamespace
1511
1612
IPv6 is supported via the standard representation::
1713
1814
pq://[::1]:5432/database
15+
16+
Driver Parameters:
17+
18+
pq://user@host/?[driver_param]=value&[other_param]=value
1919
"""
2020
from .resolved import riparse as ri
21+
import re
22+
23+
escape_path_re = re.compile('[%s]' %(re.escape(ri.unescaped + ','),))
2124

22-
def structure(t):
23-
'Create a dictionary of connection parameters from a six-tuple'
24-
d = {}
25-
if t[1] is not None:
26-
uphp = ri.split_netloc(t[1])
27-
if uphp[0]:
28-
d['user'] = uphp[0]
29-
if uphp[1]:
30-
d['password'] = uphp[1]
25+
def structure(d, fieldproc = ri.unescape):
26+
'Create a clientparams dictionary from a parsed RI'
27+
if d.get('scheme', 'pq') != 'pq':
28+
raise ValueError("not a PQ-IRI")
29+
30+
cpd = {
31+
k : fieldproc(v) for k, v in d.items()
32+
if k not in ('path', 'fragment', 'query', 'host', 'scheme')
33+
}
34+
path = d.get('path')
35+
frag = d.get('fragment')
36+
query = d.get('query')
37+
host = d.get('host')
38+
39+
if host:
40+
if host.startswith('[') and host.endswith(']'):
41+
cpd['ipv'] = 6
42+
cpd['host'] = host[1:-1]
3143
else:
32-
if uphp[2]:
33-
d['host'] = uphp[2]
34-
if uphp[3]:
35-
d['port'] = int(uphp[3])
44+
cpd['host'] = fieldproc(host)
45+
46+
if path:
47+
if len(path) > 1:
48+
raise ValueError("PQ-IRIs may only have one path component")
49+
# Only state the database field's existence if the first path is non-empty.
50+
if path[0]:
51+
cpd['database'] = path[0]
3652

37-
if t[2] is not None:
38-
d['database'] = t[2]
53+
if frag:
54+
d['path'] = [
55+
fieldproc(x) for x in frag.split(',')
56+
]
3957

40-
if t[3] is not None:
41-
d['settings'] = dict([
42-
[ri.unescape(y) for y in x.split('=', 1)]
43-
for x in t[3].split('&')
44-
])
58+
if query:
59+
settings = {}
60+
for k, v in query.items():
61+
if k.startswith('[') and k.endswith(']'):
62+
k = k[1:-1]
63+
if k != 'settings' and k not in cpd:
64+
cpd[fieldproc(k)] = fieldproc(v)
65+
elif k:
66+
settings[fieldproc(k)] = fieldproc(v)
67+
# else: ignore empty query keys
68+
if settings:
69+
cpd['settings'] = settings
4570

46-
# Path support
47-
if t[4] is not None:
48-
d['path'] = t[4].split(',')
71+
return cpd
4972

50-
return d
73+
def construct_path(x, re = escape_path_re):
74+
"""
75+
Join a path sequence using ',' and escaping ',' in the pieces.
76+
"""
77+
return ','.join((
78+
re.sub(ri.re_pct_encode, y) for y in x
79+
))
5180

5281
def construct(x):
53-
'Construct a IRI tuple from a dictionary object'
82+
'Construct a RI dictionary from a clientparams dictionary'
5483
return (
55-
"pq",
84+
'pq',
5685
# netloc: user:pass@{host[:port]|process}
5786
ri.unsplit_netloc((
5887
x.get('user'),
5988
x.get('password'),
60-
x.get('host'),
89+
'[' + x.get('host') + ']' if (
90+
str(x.get('ipv', -1)) == '6' and ':' in x.get('host', '')
91+
) else x.get('host'),
6192
x.get('port')
6293
)),
63-
ri.escape_path_re.sub(x.get('database') or '', '/'),
94+
None if 'database' not in x else (
95+
ri.escape_path_re.sub(x['database'], '/')
96+
),
6497
None if 'settings' not in x else (
65-
'&'.join([
66-
'%s=%s' %(k, v.replace('&','%26'))
67-
for k, v in x['settings'].items()
68-
])
98+
ri.construct_query(x['settings'])
6999
),
70-
None if 'path' not in x else ','.join(x['path'])
100+
None if 'path' not in x else construct_path(x['path']),
71101
)
72102

73-
def parse(s):
103+
def parse(s, fieldproc = ri.unescape):
74104
'Parse a Postgres IRI into a dictionary object'
75-
return structure(ri.split(s))
105+
return structure(
106+
# In ri.parse, don't unescape the parsed values as our sub-structure
107+
# uses the escape mechanism in IRIs to specify literal separator
108+
# characters.
109+
ri.parse(s, fieldproc = str),
110+
fieldproc = fieldproc
111+
)
76112

77113
def serialize(x):
78114
'Return a Postgres IRI from a dictionary object.'

0 commit comments

Comments
 (0)