forked from ema/flask-moresql
-
Notifications
You must be signed in to change notification settings - Fork 0
/
flask_moresql.py
145 lines (113 loc) · 4.39 KB
/
flask_moresql.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# -*- coding: utf-8 -*-
"""
flask_moresql
=============
A thin layer of glue between PostgreSQL stored procedures and Flask web
applications.
:copyright: (C) 2012 by Emanuele Rocca.
:license: BSD, see LICENSE for more details.
"""
import re
import urllib
import datetime
import simplejson
import psycopg2
import psycopg2.extras
from flask import request, make_response
def parse_rfc1738_args(name):
"""Parse the given database URI and return a dictionary of database
connection values.
Eg: parse_rfc1738_args('postgres://user:pass@host:5432/dbname')
-> {
'username': 'user',
'password': 'pass',
'host': 'host',
'port': '5432',
'database': 'dbname'
}
"""
if name is None:
raise RuntimeError("MORESQL_DATABASE_URI needs to be specified")
pattern = re.compile(r'''
(?P<name>[\w\+]+)://
(?:
(?P<username>[^:/]*)
(?::(?P<password>[^/]*))?
@)?
(?:
(?P<host>[^/:]*)
(?::(?P<port>[^/]*))?
)?
(?:/(?P<database>.*))?
''', re.X)
match = pattern.match(name)
if match is None:
raise RuntimeError("Wrong DB URI string '%s'" % name)
components = match.groupdict()
if components['name'] != 'postgres':
raise RuntimeError("MoreSQL only supports postgres databases")
if components['password'] is not None:
components['password'] = urllib.unquote_plus(components['password'])
del components['name']
return components
def _convert_http_value(value):
"""Try to parse the given JSON value. Return raw value on failure."""
try:
return simplejson.loads(value)
except simplejson.JSONDecodeError:
return value
def _get_procedure_arguments(fields, values):
"""Return a list of parameters to be passed to the stored procedure."""
if fields is None:
# The stored procedure has been called without parameters
return []
if values:
# Use user-supplied values
return [ values.get(field) for field in fields ]
# Use HTTP request values
return [ _convert_http_value(request.values.get(field))
for field in fields if request.values.get(field) is not None ]
def dthandler(obj):
if isinstance(obj, datetime.datetime):
return obj.isoformat()
class MoreSQL(object):
"""Used to connect to a given PostgreSQL database.
To use MoreSQL you need to first create a Flask application, and then bind
a MoreSQL instance to it::
app = Flask(__name__)
db = MoreSQL(app)
"""
def __init__(self, app):
self.app = app
creds = parse_rfc1738_args(app.config.get('MORESQL_DATABASE_URI'))
self.connection = psycopg2.connect(user=creds['username'],
password=creds['password'],
dbname=creds['database'],
host=creds['host'],
port=creds['port'])
self.cursor = self.connection.cursor(
cursor_factory=psycopg2.extras.RealDictCursor)
def execute(self, procname, fields=None, values=None):
"""Execute the given stored procedure. Return results as a JSON
HTTP response.
:param procname: the stored procedure name
:param fields: a list of dictionary fields used as parameters of the
stored procedure. The procedure will be called with no
arguments if omitted
:param values: an optional dictionary of values from which the
parameters should be taken. If omitted, default to the
values passed via HTTP
"""
procargs = _get_procedure_arguments(fields, values)
result = self.cursor.callproc(procname, procargs)
self.connection.commit()
if procargs == result:
# The parameters have not been modified. There was no in/out
# parameter, we have to fetch returned results from the cursor.
result = self.cursor.fetchall()
if len(result) == 1:
if len(result[0]) == 1:
result = result[0].values()[0]
else:
result = result[0]
return make_response(simplejson.dumps(result, default=dthandler))