From 7c6f7b3abb47816443d75cd42db99d88b3eafbbd Mon Sep 17 00:00:00 2001 From: Marcel Hellkamp Date: Thu, 1 Sep 2011 19:21:53 +0200 Subject: [PATCH] Moved plugins from main bottle repository to bottle-extras. --- README | 87 ++++++++++++++++++++++++++++++++++++++++++ bottle_sqlite.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 45 ++++++++++++++++++++++ test.py | 33 ++++++++++++++++ 4 files changed, 264 insertions(+) create mode 100644 README create mode 100644 bottle_sqlite.py create mode 100644 setup.py create mode 100644 test.py diff --git a/README b/README new file mode 100644 index 0000000..31f6756 --- /dev/null +++ b/README @@ -0,0 +1,87 @@ +===================== +Bottle-SQLite +===================== + +SQLite is a self-contained SQL database engine that runs locally and does not +require any additional server software or setup. The sqlite3 module is part of the +Python standard library and already installed on most systems. It it very useful +for prototyping database-driven applications that are later ported to larger +databases such as PostgreSQL or MySQL. + +This plugin simplifies the use of sqlite databases in your Bottle applications. +Once installed, all you have to do is to add a ``db`` keyword argument +(configurable) to route callbacks that need a database connection. + +Installation +=============== + +Install with one of the following commands:: + + $ pip install bottle-sqlite + $ easy_install bottle-sqlite + +or download the latest version from github:: + + $ git clone git://github.com/defnull/bottle.git + $ cd bottle/plugins/sqlite + $ python setup.py install + +Usage +=============== + +Once installed to an application, the plugin passes an open +:class:`sqlite3.Connection` instance to all routes that require a ``db`` keyword +argument:: + + import bottle + + app = bottle.Bottle() + plugin = bottle.ext.sqlite.Plugin(dbfile='/tmp/test.db') + app.install(plugin) + + @app.route('/show/:item') + def show(item, db): + row = db.execute('SELECT * from items where name=?', item).fetchone() + if row: + return template('showitem', page=row) + return HTTPError(404, "Page not found") + +Routes that do not expect a ``db`` keyword argument are not affected. + +The connection handle is configured so that :class:`sqlite3.Row` objects can be +accessed both by index (like tuples) and case-insensitively by name. At the end of +the request cycle, outstanding transactions are committed and the connection is +closed automatically. If an error occurs, any changes to the database since the +last commit are rolled back to keep the database in a consistent state. + +Configuration +============= + +The following configuration options exist for the plugin class: + +* **dbfile**: Database filename (default: in-memory database). +* **keyword**: The keyword argument name that triggers the plugin (default: 'db'). +* **autocommit**: Whether or not to commit outstanding transactions at the end of the request cycle (default: True). +* **dictrows**: Whether or not to support dict-like access to row objects (default: True). + +You can override each of these values on a per-route basis:: + + @app.route('/cache/:item', sqlite={'dbfile': ':memory:'}) + def cache(item, db): + ... + +or install two plugins with different ``keyword`` settings to the same application:: + + app = bottle.Bottle() + test_db = bottle.ext.sqlite.Plugin(dbfile='/tmp/test.db') + cache_db = bottle.ext.sqlite.Plugin(dbfile=':memory:', keyword='cache') + app.install(test_db) + app.install(cache_db) + + @app.route('/show/:item') + def show(item, db): + ... + + @app.route('/cache/:item') + def cache(item, cache): + ... diff --git a/bottle_sqlite.py b/bottle_sqlite.py new file mode 100644 index 0000000..d79b592 --- /dev/null +++ b/bottle_sqlite.py @@ -0,0 +1,99 @@ +''' +Bottle-sqlite is a plugin that integrates SQLite3 with your Bottle +application. It automatically connects to a database at the beginning of a +request, passes the database handle to the route callback and closes the +connection afterwards. + +To automatically detect routes that need a database connection, the plugin +searches for route callbacks that require a `db` keyword argument +(configurable) and skips routes that do not. This removes any overhead for +routes that don't need a database connection. + +Usage Example:: + + import bottle + from bottle.ext import sqlite + + app = bottle.Bottle() + plugin = sqlite.Plugin(dbfile='/tmp/test.db') + app.install(plugin) + + @app.route('/show/:item') + def show(item, db): + row = db.execute('SELECT * from items where name=?', item).fetchone() + if row: + return template('showitem', page=row) + return HTTPError(404, "Page not found") +''' + +__author__ = "Marcel Hellkamp" +__version__ = '0.1.1' +__license__ = 'MIT' + +### CUT HERE (see setup.py) + +import sqlite3 +import inspect +from bottle import HTTPError + + +class SQLitePlugin(object): + ''' This plugin passes an sqlite3 database handle to route callbacks + that accept a `db` keyword argument. If a callback does not expect + such a parameter, no connection is made. You can override the database + settings on a per-route basis. ''' + + name = 'sqlite' + + def __init__(self, dbfile=':memory:', autocommit=True, dictrows=True, + keyword='db'): + self.dbfile = dbfile + self.autocommit = autocommit + self.dictrows = dictrows + self.keyword = keyword + + def setup(self, app): + ''' Make sure that other installed plugins don't affect the same + keyword argument.''' + for other in app.plugins: + if not isinstance(other, SQLitePlugin): continue + if other.keyword == self.keyword: + raise PluginError("Found another sqlite plugin with "\ + "conflicting settings (non-unique keyword).") + + def apply(self, callback, context): + # Override global configuration with route-specific values. + conf = context['config'].get('sqlite') or {} + dbfile = conf.get('dbfile', self.dbfile) + autocommit = conf.get('autocommit', self.autocommit) + dictrows = conf.get('dictrows', self.dictrows) + keyword = conf.get('keyword', self.keyword) + + # Test if the original callback accepts a 'db' keyword. + # Ignore it if it does not need a database handle. + args = inspect.getargspec(context['callback'])[0] + if keyword not in args: + return callback + + def wrapper(*args, **kwargs): + # Connect to the database + db = sqlite3.connect(dbfile) + # This enables column access by name: row['column_name'] + if dictrows: db.row_factory = sqlite3.Row + # Add the connection handle as a keyword argument. + kwargs[keyword] = db + + try: + rv = callback(*args, **kwargs) + if autocommit: db.commit() + except sqlite3.IntegrityError, e: + db.rollback() + raise HTTPError(500, "Database Error", e) + finally: + db.close() + return rv + + # Replace the route callback with the wrapped one. + return wrapper + +Plugin = SQLitePlugin \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6b4af7b --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import sys +import os +from distutils.core import setup + +try: + from distutils.command.build_py import build_py_2to3 as build_py +except ImportError: + from distutils.command.build_py import build_py + +# This ugly hack executes the first few lines of the module file to look up some +# common variables. We cannot just import the module because it depends on other +# modules that might not be installed yet. +filename = os.path.join(os.path.dirname(__file__), 'bottle_sqlite.py') +source = open(filename).read().split('### CUT HERE')[0] +exec(source) + +setup( + name = 'bottle-sqlite', + version = __version__, + url = 'http://bottlepy.org/docs/dev/plugins/sqlite.html', + description = 'SQLite3 integration for Bottle.', + long_description = __doc__, + author = 'Marcel Hellkamp', + author_email = 'marc@gsites.de', + license = __license__, + platforms = 'any', + py_modules = [ + 'bottle_sqlite' + ], + requires = [ + 'bottle (>=0.9)' + ], + classifiers = [ + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Libraries :: Python Modules' + ], + cmdclass = {'build_py': build_py} +) diff --git a/test.py b/test.py new file mode 100644 index 0000000..d4024ec --- /dev/null +++ b/test.py @@ -0,0 +1,33 @@ +import unittest +import os +import bottle +from bottle.ext import sqlite +import sqlite3 + +class SQLiteTest(unittest.TestCase): + def setUp(self): + self.app = bottle.Bottle(catchall=False) + + def test_with_keyword(self): + self.plugin = self.app.install(sqlite.Plugin()) + + @self.app.get('/') + def test(db): + self.assertEqual(type(db), type(sqlite3.connect(':memory:'))) + self.app({'PATH_INFO':'/', 'REQUEST_METHOD':'GET'}, lambda x, y: None) + + def test_without_keyword(self): + self.plugin = self.app.install(sqlite.Plugin()) + + @self.app.get('/') + def test(): + pass + self.app({'PATH_INFO':'/', 'REQUEST_METHOD':'GET'}, lambda x, y: None) + + @self.app.get('/2') + def test(**kw): + self.assertFalse('db' in kw) + self.app({'PATH_INFO':'/2', 'REQUEST_METHOD':'GET'}, lambda x, y: None) + +if __name__ == '__main__': + unittest.main()