Skip to content

Commit 7e193b5

Browse files
committed
Server DB implemented
Signed-off-by: Jonas Kalderstam <[email protected]>
1 parent 08c7120 commit 7e193b5

File tree

5 files changed

+207
-19
lines changed

5 files changed

+207
-19
lines changed

server-app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
*.pyc
2+
*.db

server-app/app.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,81 @@
1-
import bottle
2-
from bottle import run, get, post, delete
1+
from hashlib import sha1
2+
from bottle import run, get, post, delete, install, HTTPError, request
3+
from bottle_sqlite import SQLitePlugin
4+
5+
from dbsetup import init_db
6+
7+
8+
DBNAME='test.db'
9+
10+
init_db(DBNAME)
11+
install(SQLitePlugin(dbfile=DBNAME))
12+
13+
def to_dict(row):
14+
return dict(sha=row['sha'],
15+
url=row['url'],
16+
timestamp=row['timestamp'],
17+
# Convert integer to boolean
18+
deleted=(1 == row['deleted']))
19+
320

421
@get('/')
522
@get('/links')
6-
def list_links():
23+
def list_links(db):
724
'''Return a complete list of all links'''
8-
return dict(links=[])
25+
links = []
26+
for row in db.execute('SELECT * from links'):
27+
links.append(to_dict(row))
28+
return dict(links=links)
929

10-
@get('/links/<id>')
11-
def get_link(id):
30+
@get('/links/<sha>')
31+
def get_link(db, sha):
1232
'''Returns a specific link'''
13-
return dict(link={"sha":"1111111",
14-
"url":"http://www.google.com",
15-
"timestamp":"2013-09-19 08:22:19.000"})
33+
row = db.execute('SELECT * from links WHERE sha IS ?', [sha]).fetchone()
34+
if row:
35+
return dict(link=to_dict(row))
36+
37+
return HTTPError(404, "No such item")
1638

17-
@delete('/links/<id>')
18-
def delete_link(id):
39+
@delete('/links/<sha>')
40+
def delete_link(db, sha):
1941
'''Deletes a specific link from the list.
2042
On success, returns an empty response'''
21-
return {}
43+
db.execute('UPDATE links SET deleted = 1, timestamp = CURRENT_TIMESTAMP \
44+
WHERE sha IS ? AND userid is ?', [sha, userid])
45+
46+
if db.total_changes > 0:
47+
return {}
48+
49+
return HTTPError(404, "No such item")
2250

2351
@post('/links')
24-
def add_link():
52+
def add_link(db):
2553
'''Adds a link to the list.
2654
On success, returns the entry created.'''
27-
return dict(link={"sha":"1111111",
28-
"url":"http://www.google.com",
29-
"timestamp":"2013-09-19 08:22:19.000"})
55+
# Only accept json data
56+
if request.content_type != 'application/json':
57+
return HTTPError(415, "Only json is accepted")
58+
# Check required fields
59+
if 'url' not in request.json:
60+
return HTTPError(400, "Must specify a url")
61+
62+
# Sha is optional, generate if not present
63+
if 'sha' not in request.json:
64+
request.json['sha'] = sha1(request.json['url']).hexdigest()
65+
66+
args = [request.json['url'],
67+
request.json['sha']]
68+
if 'timestamp' in request.json:
69+
args.append(request.json['timestamp'])
70+
stmt = 'INSERT INTO links (url, sha, timestamp) VALUES(?, ?, ?)'
71+
else:
72+
stmt = 'INSERT INTO links (url, sha) VALUES(?, ?)'
73+
74+
db.execute(stmt, args)
75+
76+
return get_link(db, request.json['sha'])
77+
3078

3179
if __name__ == '__main__':
32-
bottle.debug(True)
33-
run(port=5500)
80+
# Restart server automatically when this file changes
81+
run(host='0.0.0.0', port=5500, reloader=True, debug=True)

server-app/bottle_sqlite.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'''
2+
Bottle-sqlite is a plugin that integrates SQLite3 with your Bottle
3+
application. It automatically connects to a database at the beginning of a
4+
request, passes the database handle to the route callback and closes the
5+
connection afterwards.
6+
7+
To automatically detect routes that need a database connection, the plugin
8+
searches for route callbacks that require a `db` keyword argument
9+
(configurable) and skips routes that do not. This removes any overhead for
10+
routes that don't need a database connection.
11+
12+
Usage Example::
13+
14+
import bottle
15+
from bottle.ext import sqlite
16+
17+
app = bottle.Bottle()
18+
plugin = sqlite.Plugin(dbfile='/tmp/test.db')
19+
app.install(plugin)
20+
21+
@app.route('/show/:item')
22+
def show(item, db):
23+
row = db.execute('SELECT * from items where name=?', item).fetchone()
24+
if row:
25+
return template('showitem', page=row)
26+
return HTTPError(404, "Page not found")
27+
'''
28+
29+
__author__ = "Marcel Hellkamp"
30+
__version__ = '0.1.2'
31+
__license__ = 'MIT'
32+
33+
### CUT HERE (see setup.py)
34+
35+
import sqlite3
36+
import inspect
37+
from bottle import HTTPResponse, HTTPError
38+
39+
40+
class SQLitePlugin(object):
41+
''' This plugin passes an sqlite3 database handle to route callbacks
42+
that accept a `db` keyword argument. If a callback does not expect
43+
such a parameter, no connection is made. You can override the database
44+
settings on a per-route basis. '''
45+
46+
name = 'sqlite'
47+
48+
def __init__(self, dbfile=':memory:', autocommit=True, dictrows=True,
49+
keyword='db'):
50+
self.dbfile = dbfile
51+
self.autocommit = autocommit
52+
self.dictrows = dictrows
53+
self.keyword = keyword
54+
55+
def setup(self, app):
56+
''' Make sure that other installed plugins don't affect the same
57+
keyword argument.'''
58+
for other in app.plugins:
59+
if not isinstance(other, SQLitePlugin): continue
60+
if other.keyword == self.keyword:
61+
raise PluginError("Found another sqlite plugin with "\
62+
"conflicting settings (non-unique keyword).")
63+
64+
def apply(self, callback, context):
65+
# Override global configuration with route-specific values.
66+
conf = context['config'].get('sqlite') or {}
67+
dbfile = conf.get('dbfile', self.dbfile)
68+
autocommit = conf.get('autocommit', self.autocommit)
69+
dictrows = conf.get('dictrows', self.dictrows)
70+
keyword = conf.get('keyword', self.keyword)
71+
72+
# Test if the original callback accepts a 'db' keyword.
73+
# Ignore it if it does not need a database handle.
74+
args = inspect.getargspec(context['callback'])[0]
75+
if keyword not in args:
76+
return callback
77+
78+
def wrapper(*args, **kwargs):
79+
# Connect to the database
80+
db = sqlite3.connect(dbfile)
81+
# This enables column access by name: row['column_name']
82+
if dictrows: db.row_factory = sqlite3.Row
83+
# Add the connection handle as a keyword argument.
84+
kwargs[keyword] = db
85+
86+
try:
87+
rv = callback(*args, **kwargs)
88+
if autocommit: db.commit()
89+
except sqlite3.IntegrityError, e:
90+
db.rollback()
91+
raise HTTPError(500, "Database Error", e)
92+
except HTTPError, e:
93+
raise
94+
except HTTPResponse, e:
95+
if autocommit: db.commit()
96+
raise
97+
finally:
98+
db.close()
99+
return rv
100+
101+
# Replace the route callback with the wrapped one.
102+
return wrapper
103+
104+
Plugin = SQLitePlugin

server-app/curl_tests.sh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,11 @@ echo "\n\nGetting one"
1010
curl -X GET http://localhost:5500/links/42
1111

1212
echo "\n\nAdding one"
13-
curl -X POST http://localhost:5500/links
13+
curl -X POST -H "Content-Type: application/json" -d '{"url":"http://www.google.com","username":"xyz","password":"xyz"}' http://localhost:5500/links
14+
15+
echo "\n\nAdding wrong header"
16+
curl -X POST -d '{"username":"xyz","password":"xyz"}' http://localhost:5500/links
17+
18+
19+
echo "\n\nAdding no url"
20+
curl -X POST -H "Content-Type: application/json" -d '{"username":"xyz","password":"xyz"}' http://localhost:5500/links

server-app/dbsetup.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import sqlite3 as sql
2+
import sys
3+
4+
_CREATE_TABLE = \
5+
"""CREATE TABLE IF NOT EXISTS links
6+
(_id INTEGER PRIMARY KEY,
7+
userid TEXT NOT NULL,
8+
sha TEXT NOT NULL,
9+
url TEXT NOT NULL,
10+
deleted INTEGER NOT NULL DEFAULT 0,
11+
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
12+
13+
UNIQUE(userid, url) ON CONFLICT REPLACE,
14+
UNIQUE(userid, sha) ON CONFLICT REPLACE)
15+
"""
16+
17+
def init_db(filename='test.db'):
18+
con = sql.connect(filename)
19+
con.row_factory = sql.Row
20+
with con:
21+
cur = con.cursor()
22+
cur.execute(_CREATE_TABLE)
23+
24+
if __name__ == '__main__':
25+
if len(sys.argv) > 1:
26+
init_db(sys.argv[1])
27+
else:
28+
init_db()

0 commit comments

Comments
 (0)