diff options
author | michele.simionato <devnull@localhost> | 2008-12-29 06:00:23 +0000 |
---|---|---|
committer | michele.simionato <devnull@localhost> | 2008-12-29 06:00:23 +0000 |
commit | cc2fea83562bc9b73d19b53584e07eee676d91fa (patch) | |
tree | 47f53a05160a89180defdc598551c7a76dbeeede /sqlplain | |
parent | 6c5dc2dd4d7e0da06db0044c8a854df6483f13cd (diff) | |
download | micheles-cc2fea83562bc9b73d19b53584e07eee676d91fa.tar.gz |
Refactoring of sqlplain (removed connmethod and made tables more OO)
Diffstat (limited to 'sqlplain')
-rw-r--r-- | sqlplain/automatize.py | 16 | ||||
-rw-r--r-- | sqlplain/configurator.py | 83 | ||||
-rw-r--r-- | sqlplain/connection.py | 33 | ||||
-rw-r--r-- | sqlplain/doc/book_importer.py | 46 | ||||
-rw-r--r-- | sqlplain/doc/book_importer.py~ | 37 | ||||
-rw-r--r-- | sqlplain/doc/books1.sql | 41 | ||||
-rw-r--r-- | sqlplain/doc/books1.sql~ | 32 | ||||
-rw-r--r-- | sqlplain/doc/cache_ex.py~ | 28 | ||||
-rw-r--r-- | sqlplain/doc/compare_tables.py | 32 | ||||
-rw-r--r-- | sqlplain/doc/compare_tables.py~ | 32 | ||||
-rw-r--r-- | sqlplain/doc/doc.py | 124 | ||||
-rw-r--r-- | sqlplain/doc/first-page.txt | 125 | ||||
-rw-r--r-- | sqlplain/doc/first-page.txt~ | 138 | ||||
-rw-r--r-- | sqlplain/doc/logtable.py | 14 | ||||
-rw-r--r-- | sqlplain/doc/logtable.py~ | 12 | ||||
-rw-r--r-- | sqlplain/doc/queries.py~ | 5 | ||||
-rw-r--r-- | sqlplain/doc/tables.py | 90 | ||||
-rw-r--r-- | sqlplain/doc/tables.py~ | 92 | ||||
-rw-r--r-- | sqlplain/mssql_util.py | 3 | ||||
-rw-r--r-- | sqlplain/postgres_util.py | 4 | ||||
-rw-r--r-- | sqlplain/sqlite_util.py | 3 | ||||
-rw-r--r-- | sqlplain/table.py | 159 | ||||
-rw-r--r-- | sqlplain/util.py | 11 |
23 files changed, 888 insertions, 272 deletions
diff --git a/sqlplain/automatize.py b/sqlplain/automatize.py index 8b1d267..0aded29 100644 --- a/sqlplain/automatize.py +++ b/sqlplain/automatize.py @@ -8,7 +8,7 @@ Chunk = namedtuple('Chunk', 'version fname code') try: CalledProcessError = subprocess.CalledProcessError -except AttributeError: +except AttributeError: # Python < 2.5 class CalledProcessError(Exception): pass def getoutput(commandlist): @@ -44,6 +44,20 @@ def make_db(alias=None, uri=None, dir=None): db.execute(chunk.code) elif chunk.fname.endswith('.py'): exec chunk.code in {} + +def populate_db(db, dir=None): + uri = db.uri + if dir is None: + alias = configurator.alias[uri] + dir = configurator.dir[alias] + else: + raise TypeError('Please provide a scriptdir') + chunks = collect(dir, ('.sql', '.py')) + for chunk in chunks: + if chunk.fname.endswith('.sql'): + db.execute(chunk.code) + elif chunk.fname.endswith('.py'): + exec chunk.code in {} def make_schema(alias=None, schema=None, uri=None, dir=None): if alias is not None and uri is None: diff --git a/sqlplain/configurator.py b/sqlplain/configurator.py index 2c7f31b..1e6be43 100644 --- a/sqlplain/configurator.py +++ b/sqlplain/configurator.py @@ -20,72 +20,47 @@ import UserDict from ConfigParser import RawConfigParser # no magic interpolation from cStringIO import StringIO -class ConfigObj(UserDict.DictMixin): - # spaces are ignored and options are case-insensitive - """Convert a configuration file or string into a dictionary-like object - sections -> items. For instance - - cfg = ConfigObj(''' - [fields] - title: char(32) - score: char(6) - author: char(32) - date: char(8) - genre: char(4) - nation: char(4) - ''') - print cfg.fields - """ - def __init__(self, config_str_or_file): - if isinstance(config_str_or_file, basestring): - # dedent and convert into a file-like object - cfg = StringIO(textwrap.dedent(config_str_or_file)) +class ReadOnlyObject(object): + def __init__(self, items): + keys = [] + for name, value in items: + if name.startswith('_'): + raise TypeError('Inner attributes cannot begin with "_"') + object.__setattr__(self, name, value) + keys.append(name) + object.__setattr__(self, '_dic', dic) + object.__setattr__(self, '_keys', keys) + def __iter__(self): + for name in self._keys: + yield name, getattr(self, name) + def __contains__(self, name): + return name in self._keys + def __len__(self): + return self(self._keys) + def __setattr__(self, name, value): + if name in self._dic: + raise TypeError('Read-only object!') else: - cfg = config_str_or_file # assume cfg is a file-like object\ - try: - cfp = RawConfigParser() - cfp.readfp(cfg) - self._keys = [] - for sect in cfp.sections(): - setattr(self, sect, dict(cfp.items(sect))) - self._keys.append(sect) - finally: - cfg.close() + object.__setattr__(self, name, value) + def __str__(self): + return '\n'.join('%s=%s' % (k, v) for k, v in self) - def __getitem__(self, name): - return getattr(self, name) - - def iteritems(self): - for k in self._keys: - yield k, getattr(self, k) - - def keys(self): - return self._keys - - def textrepr(self): - for sect, settings in self.iteritems(): - yield '\n[%s]\n' % sect - for k, v in settings.iteritems(): - yield '%s=%s' % (k, v) - - def save(self, fname): - f = file(fname, 'w') - for line in self.textrepr(): - print >> f, line - f.close() - class _Configurator(object): # singleton _initialized = False def _initialize(self): + cfp = RawConfigParser() self._conf_file = os.environ.get( 'SQLPLAIN', os.path.expanduser('~/.sqlplain')) - self._conf_obj = ConfigObj(file(self._conf_file)) + cfp.readfp(file(self._conf_file)) + self._conf_obj = ReadOnlyObject( + (sect, ReadOnlyObject(cfp.items(sect))) + for sect in cfp.sections()) self._initialized = True def __getattr__(self, name): if not self._initialized: self._initialize() - return self._conf_obj[name] + return getattr(self._conf_obj, name) configurator = _Configurator() diff --git a/sqlplain/connection.py b/sqlplain/connection.py index 2eac05c..63e1087 100644 --- a/sqlplain/connection.py +++ b/sqlplain/connection.py @@ -48,7 +48,7 @@ class Transaction(object): conn.rollback() class _Storage(object): - "A place where to store low level connection and cursor" + "A place where to store the low level connection and cursor" @classmethod def new(cls, connect, args): @@ -119,7 +119,11 @@ class LazyConnection(object): self.driver.ProgrammingError, self.driver.InterfaceError, self.driver.DatabaseError) - + + def open(self): + "Return the low level underlying connection" + return self._storage.conn + def _raw_execute(self, cursor, templ, args): """ Call a dbapi2 cursor; return the rowcount or a list of tuples, @@ -152,7 +156,7 @@ class LazyConnection(object): except self.errors, e: # maybe bad connection self.close() # reset connection and retry print '%s, retrying' % e - return raw_execute(self._curs, templ, args) + return raw_execute(self._storage.curs, templ, args) def execute(self, templ, args=(), ntuple=None, scalar=False): if self.dbtype == 'mssql': @@ -171,7 +175,7 @@ class LazyConnection(object): raise TypeError("Expected %d arguments, got %d: %s" % ( qmarks, len(args), args)) - descr, res = self._execute(self._curs, templ, args) + descr, res = self._execute(self._storage.curs, templ, args) if scalar: # you expect a scalar result if not res: raise KeyError( @@ -181,7 +185,7 @@ class LazyConnection(object): "Expected to get a scalar result, got %s\nQUERY WAS:%s%s\n" % (res, templ, args)) return res[0][0] - cursor = self._curs # needed to make the reset work + cursor = self._storage.curs # needed to make the reset work if self.chatty: print(cursor.rowcount, templ, args) if descr: @@ -206,7 +210,7 @@ class LazyConnection(object): if d: sql = string.Template(sql).substitute(d) if self.dbtype == 'sqlite': - self._curs.executescript(sql) + self._storage.curs.executescript(sql) else: # psycopg and pymssql are already able to execute chunks self.execute(sql) @@ -217,16 +221,6 @@ class LazyConnection(object): def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.uri) - - @property - def _conn(self): - "Return the low level underlying connection" - return self._storage.conn - - @property - def _curs(self): - "Return the low level underlying cursor" - return self._storage.curs class TransactionalConnection(LazyConnection): """ @@ -235,10 +229,10 @@ class TransactionalConnection(LazyConnection): """ def rollback(self): - return self._conn.rollback() + return self._storage.conn.rollback() def commit(self): - return self._conn.commit() + return self._storage.conn.commit() def __enter__(self): return self @@ -274,8 +268,7 @@ class NullObject(object): class FakeConnection(object): def __init__(self, iodict): self.iodict = iodict - self._conn = NullObject() - self._curs = NullObject() + self._storage = NullObject() def execute(self, templ, args=()): return self.iodict[(templ,) + args] def executescript(self, templ, *dicts, **kw): diff --git a/sqlplain/doc/book_importer.py b/sqlplain/doc/book_importer.py new file mode 100644 index 0000000..8935ec6 --- /dev/null +++ b/sqlplain/doc/book_importer.py @@ -0,0 +1,46 @@ +import re, csv, itertools +from sqlplain.namedtuple import namedtuple + +class BookImporter(object): + + # format of the CSV files to be imported + BOOKFILE = re.compile('books(\d\d).csv') + Book = namedtuple('title author genre nation date') + + def __init__(self, db): + self.db = db + self.bookid = KTable(db, 'bookid') + self.book = KTable(db, 'book') + self.score = dict(KTable(db, 'score')) + self.genre = KTable(db, 'genre') + self.nation = KTable(db, 'nation') + self.counter = itertools.count(1) + + def import_books(self, books): + for b in books: + self.bookid.insert_row( + id = self.counter.next(), + title = b.title, + author = b.author, + rdate = b.date) + self.book.insert_row( + id = id, + genre = b.genre, + nation = b.nation, + score = self.scoredic[b.score]) + + def import_all(self, dirname=None): + dirname = dirname or self.db.uri.scriptdir + for fname in os.listdir(dirname): + mo = self.BOOKFILE.match(fname) + f = file(os.path.join(dirname, fname)) + if mo: + books = map(self.Book, csv.reader(f)) + f.close() + self.import_books(books) + + +if __name__ == '__main__': + db = lazyconnect('bookdb') + imp = BookImporter(db) + imp.import_all() diff --git a/sqlplain/doc/book_importer.py~ b/sqlplain/doc/book_importer.py~ new file mode 100644 index 0000000..1398cbe --- /dev/null +++ b/sqlplain/doc/book_importer.py~ @@ -0,0 +1,37 @@ +import re, csv, itertools +from sqlplain.namedtuple import namedtuple + +Book = namedtuple('title author genre nation date') + +BOOKFILE = re.compile('books(\d\d)') + +class BookImporter(object): + def __init__(self, db): + self.db = db + self.bookid = KTable.reflect(db, 'bookid') + self.book = KTable.reflect(db, 'book') + self.counter = itertools.count(1) + + def import_year(self, books, year): + for b in books: + self.bookid.insert_row( + id = self.counter.next(), + title = b.title, + author = b.author, + rdate = b.date) + + def import_all(self, dirname): + n = 0 + for fname in os.listdir(dirname): + mo = BOOKFILE.match(fname) + f = file(os.path.join(dirname, fname)) + if mo: + yy = int(mo.group(1)) + if yy > 80: + year = 1900 + yy + else: + year = 2000 + y + rows = map(Book, csv.reader(f)) + f.close() + self.import_year(rows, year) + diff --git a/sqlplain/doc/books1.sql b/sqlplain/doc/books1.sql new file mode 100644 index 0000000..e29f2aa --- /dev/null +++ b/sqlplain/doc/books1.sql @@ -0,0 +1,41 @@ +CREATE TABLE bookid ( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(128), + author VARCHAR(64), + rdate DATETIME, + UNIQUE (title, author rdate)); + +CREATE TABLE score ( + score VARCHAR(4) PRIMARY KEY, + value INTEGER); + +INSERT INTO score VALUES ('+', 1); +INSERT INTO score VALUES ('O', 2); +INSERT INTO score VALUES ('O+', 3); +INSERT INTO score VALUES ('OO', 4); +INSERT INTO score VALUES ('OO+', 5); +INSERT INTO score VALUES ('OOO', 6); +INSERT INTO score VALUES ('OOO+', 7); +INSERT INTO score VALUES ('OOOO', 8); + +CREATE TABLE genre ( + id CHAR(2) PRIMARY KEY, + descr VARCHAR(32)) + +CREATE TABLE nation ( + id CHAR(2) PRIMARY KEY, + descr VARCHAR(32)); + +CREATE TABLE book ( + id INTEGER PRIMARY KEY REFERENCES bookid (id), + genre CHAR(2) REFERENCES genre (id), + nation CHAR(2) REFERENCES nation (id), + score INTEGER REFERENCES score (value)); + +CREATE VIEW book_view AS + SELECT a.id, a.title, a.author, a.rdate, b.genre, b.nation, c.score + FROM bookid AS a + INNER JOIN book AS b + ON a.id = b.id + INNER JOIN score AS c + ON c.value=b.score; diff --git a/sqlplain/doc/books1.sql~ b/sqlplain/doc/books1.sql~ new file mode 100644 index 0000000..1320689 --- /dev/null +++ b/sqlplain/doc/books1.sql~ @@ -0,0 +1,32 @@ +CREATE TABLE bookid ( + id INTEGER AUTO_INCREMENT PRIMARY KEY, + title VARCHAR(128), + author VARCHAR(64), + rdate DATETIME, + UNIQUE (title, author rdate)); + +CREATE TABLE score ( + score VARCHAR(4) PRIMARY KEY, + value INTEGER); + +CREATE TABLE genre ( + id CHAR(2) PRIMARY KEY, + descr VARCHAR(32)) + +CREATE TABLE nation ( + id CHAR(2) PRIMARY KEY, + descr VARCHAR(32)); + +CREATE TABLE book ( + id INTEGER PRIMARY KEY REFERENCES bookid (id), + genre CHAR(2) REFERENCES genre (id), + nation CHAR(2) REFERENCES nation (id), + score INTEGER REFERENCES score (value)); + +CREATE VIEW book_view AS + SELECT a.id, a.title, a.author, a.rdate, b.genre, b.nation, c.score + FROM bookid AS a + INNER JOIN book AS b + ON a.id = b.id + INNER JOIN score AS c + ON c.value=b.score; diff --git a/sqlplain/doc/cache_ex.py~ b/sqlplain/doc/cache_ex.py~ new file mode 100644 index 0000000..f9b802b --- /dev/null +++ b/sqlplain/doc/cache_ex.py~ @@ -0,0 +1,28 @@ +from sqlplain.memoize import Memoize + +class Forever(object): + "Cache type for caches which are is never cleared" + +class LongTime(object): + "Cache type for caches which are rarely cleared" + +class ShortTime(object): + "Cache type for caches which are is often cleared" + +@Memoize(ShortTime) +def f1(): + return 1 + +@Memoize(LongTime) +def f2(): + return 2 + +@Memoize(Forever) +def f3(): + return 3 + +def test1(): + assert not f1.cache + f1() + assert f1.cache == {(): 1} + diff --git a/sqlplain/doc/compare_tables.py b/sqlplain/doc/compare_tables.py new file mode 100644 index 0000000..89911a0 --- /dev/null +++ b/sqlplain/doc/compare_tables.py @@ -0,0 +1,32 @@ +from sqlplain import lazyconnect, do +from cStringIO import StringIO +#from sqlplain.postgres_util import get_schema_postgres +#print get_schema_postgres(rcare.uri, 'customer') + +rcare = lazyconnect('rcare') +rcare_prod = lazyconnect('rcare_prod') + +CREATE_CUSTOMER = ''' +CREATE TABLE $customer ( + client_id character varying(32) PRIMARY KEY, + description character varying, + email character varying, + short_description character varying(16), + mail_report_info character varying(128), + attach_prefix character varying(32), + filter_from character varying(32), + zope_id character varying(32), + lookup boolean, + client_srs character varying(32), + CONSTRAINT nnull_id_short_desc CHECK ((short_description IS NOT NULL)) +); +''' + +def copy_from(src, dest, table): + out = StringIO() + src._curs.copy_to(out, 'customer') + drop_table(dest, 'customer_prod', force=True): + dest.executescript(CREATE_CUSTOMER, customer='customer_prod') + dest._curs.copy_from(out, 'customer_prod') + out.close() + diff --git a/sqlplain/doc/compare_tables.py~ b/sqlplain/doc/compare_tables.py~ new file mode 100644 index 0000000..f714473 --- /dev/null +++ b/sqlplain/doc/compare_tables.py~ @@ -0,0 +1,32 @@ +from sqlplain import lazyconnect +from cStringIO import StringIO +#from sqlplain.postgres_util import get_schema_postgres +#print get_schema_postgres(rcare.uri, 'customer') + +rcare = lazyconnect('rcare') +rcare_prod = lazyconnect('rcare_prod') + +CREATE_CUSTOMER = ''' +CREATE TABLE customer_prod ( + client_id character varying(32) PRIMARY KEY, + description character varying, + email character varying, + short_description character varying(16), + mail_report_info character varying(128), + attach_prefix character varying(32), + filter_from character varying(32), + zope_id character varying(32), + lookup boolean, + client_srs character varying(32), + CONSTRAINT nnull_id_short_desc CHECK ((short_description IS NOT NULL)) +); +''' + +def copy_from(src, dest, table): + out = StringIO() + src._curs.copy_to(out, 'customer') + if not exists_table( + dest.create_table('customer_prod', force=True) + dest._curs.copy_from(out, 'customer_prod') + out.close() + diff --git a/sqlplain/doc/doc.py b/sqlplain/doc/doc.py index 207abc4..0c9af99 100644 --- a/sqlplain/doc/doc.py +++ b/sqlplain/doc/doc.py @@ -68,6 +68,19 @@ plain old SQL queries, without any support from Python. .. _Django philosophy: http://docs.djangoproject.com/en/dev/misc/design-philosophies/#misc-design-philosophies +Differences with the DB API +-------------------------------------------------- + +sqlplain has a functional feeling. +i have always hated the DB API 2, particularly the fact that the execute +method does not return anyything but works by side effects, changing +the state of the cursor so that the next fetch operation returns the +retrieved rows. In sqlplain instead the eexecute method returns +directly the retried rows and there is no explicit concept of cursor. +This is not a particularly original idea, and actually the sqlite +driver offers the same functionality. The rows are returned as named +tuples, not as plain tuples. + sqlplain for the impatient --------------------------------------------------------------- @@ -451,18 +464,9 @@ operation. If you want to insert a line at the time, you can do so by using the low level mechanism ( ``conn.execute("INSERT INTO mytable VALUES (?, ?, ?)", (r1, r2, r3))``) -or by using the high level table framework discussed in the next section. - -sqlplain: extensions -================================================================= +or by using the high level `table framework`_. -``sqlplain`` is designed as a small core - just a lightweight wrapper -over the standard DB API 2 interface - and a set of extensions. -Future versions of ``sqlplain`` may offer additional extensions, but -for the moment very little is provided. I am committed to keep the -whole of ``sqlplain`` small - as I said, well under the 5% of the -codebase of sqlalchemy - so even the extension part is guaranteed to -stay small in the foreseeable future. +.. _table framework: tables.html SQL template functions -------------------------------------------------------------- @@ -609,97 +613,17 @@ something more sophisticated on top of it, but for the moment it works well enough for my needs. Future versions of ``sqlplain`` could offer additional functionality for generating SQL templates, or could not. - -The table framework ------------------------------------------------------------- - -As I said in the introduction, ``sqlplain`` is not intended to be -a fully fledged ORM, therefore it does not provide a builtin mechanism -to map user defined classes to database objects, such as the mapping -mechanism in SQLAlchemy, or the popular Active Record pattern; nevertheless, -it provides a table framework which us a lightweight object oriented layer -over database tables. -``sqlplain`` table object comes in two flavors: D-tables, which should be -used for tables without a primary key, and K-tables, which must -used for tables with a primary key, possibly composite. Actually K-tables -are D-tables too, since they inherit from D-tables, therefore they -share all D-tables methods and they add a few methods which are -meaninful only if the underlying table has a primary key. -If you try to use a K-table over a database table which has no primary -key, a ``TypeError`` will be raised. Here is an example: - -$$logtable - ->>> from sqlplain import table ->>> import logtable ->>> db = logtable.init('sqlite_test') ->>> log = table.KTable.reflect(db, 'log') -Traceback (most recent call last): - ... -TypeError: table log has no primary key! - -Using a DTable instead works: - ->>> log = table.DTable.reflect(db, 'log') - -The ``.reflect`` classmethod creates a suitable subclass of ``DTable`` -(called ``Log``) and instantiates it. ``DTable`` is a kind of abstract -base class and you cannot instantiate it directly (the same is true -for the KTable class): - ->>> table.DTable(db) -Traceback (most recent call last): - ... -TypeError: You cannot instantiate the ABC DTable - -In order to create a concrete subclass of DTable (or KTable) one needs -to set the tabletuple class attribute ``tt``, which contains information -about the table structure. The ``.reflect`` method extracts the information -from the database schema; for instance ``log.tt`` is a namedtuple with -fields ``date`` and ``message``: - ->>> print log.tt._fields -('date', 'message') - - ->>> from datetime import datetime ->>> now = datetime.now ->>> log.insert_row(date=now(), message='message1') -1 ->>> log.insert_row(date=now(), message='message2') -1 ->>> print len(log) -2 - -Here is the full API for DTable: - -- type -- create -- reflect -- select -- count -- delete -- truncate -- insert_row -- insert_rows -- insert_file - -Here are the additional methods of K-tables: - -- select_row -- update_row -- update_or_insert_row -- delete_row - -Instead of an explanation, I will give examples:: - select_book = select_row('book', 'title author') - select_book(bookdb, title='T', author='A') +sqlplain: extensions +================================================================= -``select_row`` raises an error if the corresponding queries -returns no result (you are looking for a missing record) or -if it returns multiple results (it means that your primary key specification -was incomplete). +``sqlplain`` is designed as a small core - just a lightweight wrapper +over the standard DB API 2 interface - and a set of extensions. +Future versions of ``sqlplain`` may offer additional extensions, but +for the moment very little is provided. I am committed to keep the +whole of ``sqlplain`` small - as I said, well under the 5% of the +codebase of sqlalchemy - so even the extension part is guaranteed to +stay small in the foreseeable future. Memoization ------------------------------------------------------------- diff --git a/sqlplain/doc/first-page.txt b/sqlplain/doc/first-page.txt new file mode 100644 index 0000000..19f9521 --- /dev/null +++ b/sqlplain/doc/first-page.txt @@ -0,0 +1,125 @@ +sqlplain: a simple and opinionated Databases Toolkit +==================================================================== + +sqlplain is a small Python library with the aim of simplifying your +interaction with relation databases. + +sqlplain is very much opinionated and it +does not try to be everything to everybody. It is intended to be a +middle level toolkit: higher level than the DB API 2, but lower +level than SQLAlchemy. + +It was written in part as a reaction to SQLAlchemy (only in part since I +wrote the core of sqlplain years ago, before the publication of SQLAlchemy). +SQLAlchemy is actually +an impressive piece of software but I think it is overkill for many users. +sqlplain is for users looking for a simple library. It is also fine for +database administrators with a lot of experience in SQL and little +experience in Python, since its basic mode of operation is just to run +plain old SQL scripts (hence the name). + +Its design guidelines are the following: + +- conciseness + +sqlplain is committed to keep the codebase small (say under 2000 lines) +and the number of features limited; + +- simplicity + +sqlplain is based on a simple +implementation, to make it easy for its users to understand it and to +build the features they need; + +- easyness of use + +sqlplain has a small learning curve +and provides a small set of convenient utilities for scripting your +database interation; + +- familiarity + +sqlplain connections are a simplified version of standard +DB API 2 connections, and you can readily get the underlying low +level connection to use of all its power, if need there is; + +- transparency + +sqlplain tries hard not to add too many layers of indirection to your +SQL queries: the idea is that it should be easy for the user to extract +the plain old SQL query from the code, cut and paste it on the database +console, and try it out directly. + +sqlplain can be used to do anything on a database, but there is special +support for the following areas: + +- creating test databases +- running database performance tests +- introspecting pre-existing databases +- scripting database operations +- memoizing queries + +You can use sqlplain with your Web framework of choice (I have +been running it with Pylons for years without problems) but there +is no special integration with any framework - this is an advantage +or a disadvantage depending on your point of view. Certainly sqlplain +is *not* intended to be used as the Django ORM. Django (as most +Python ORMs) puts the emphasis on Python, in the sense that the +model is written down as a set of Python classes which are then +persistent to the database. On the contrary, sqlplain assumes +that you already have your tables on the database and that they +are managed via DDL queries: you can introspect your tables +from Python, but if you want to look at their definitions you +must look into SQL code, not into Python code. There is no +facility to create tables from Python code (actually there is, +but it works by just passing plain SQL code to the creational procedure) +and no facility to modify the schema of an existing table from +Python (apart for passing raw "ALTER TABLE" queries to the connection +object). + +sqlplain does provide limited support for the following areas: + +- database independence + +True database-independence is a dream, since the features and the +performance characteristics provided by different databases are +totally different. Therefore, sqlplain does not even attempt to +be a fully database-independent toolkit. However, in the cases where +I could keep database-independence with little effort, I kept it. +In particular the introspection functionalities are database-independent. +But in general I judge spending a lot of time to provide a false sensation +of database independence not worth the effort. + +- programmatic generation of SQL queries + +Some database toolkits (notably SQLAlchemy) have an incredibly sophisticated +mechanism to generate SQL queries from Python objects in a +database-independent way. That is not the goal of +sqlplain. Out of the box sqlplain provides no support at all for generating +SQL queries programmatically, however the documentation provides a few +do-it-yourself recipes. + +- Object Relation Mapper + +The core of sqlplain does not provide any ORM. A small table framework +is provided as an experimental extension: its goal is to provide a small +object oriented wrapper over the tables of database, but it is not an +ORM such as the ones you can find in SQLAlchemy, SQLObject or Storm. +It is pretty easy to write custom classes making use of the table +framework, but you are on your own, there is nothing built-in. +This is done on purpose. + +- Connection pooling + +sqlplain does not provide any connection pooling facility and it will never +provide one. There are already many connection pool implementations available +out there and just use the one you like, if you need one. +Notice that you can use sqlplain connections in a multithreaded application +(such as a Web server) even without an explicit connection pool since the +connections can be made thread local. + +- no support for nested transactions, no unit-of-work pattern. + + + + diff --git a/sqlplain/doc/first-page.txt~ b/sqlplain/doc/first-page.txt~ new file mode 100644 index 0000000..3e0fe47 --- /dev/null +++ b/sqlplain/doc/first-page.txt~ @@ -0,0 +1,138 @@ +sqlplain: a simple and opinionated Databases Toolkit +==================================================================== + +sqlplain is a small Python library with the aim of simplifying your +interaction with relation databases. + +sqlplain is very much opinionated and it +does not try to be everything to everybody. It is intended to be a +middle level toolkit: higher level than the DB API 2, but lower +level than SQLAlchemy. + +Differences with the DB API +-------------------------------------------------- + +sqlplain has a functional feeling. +i have always hated the DB API 2, particularly the fact that the execute +method does not return anyything but works by side effects, changing +the state of the cursor so that the next fetch operation returns the +retrieved rows. In sqlplain instead the eexecute method returns +directly the retried rows and there is no explicit concept of cursor. +This is not a particularly original idea, and actually the sqlite +driver offers the same functionality. The rows are returned as named +tuples, not as raw tuples. + +It was written in part as a reaction to SQLAlchemy (only in part since I +wrote the core of sqlplain years ago, before the publication of SQLAlchemy). +SQLAlchemy is actually +an impressive piece of software but I think it is overkill for many users. +sqlplain is for users looking for a simple library. It is also fine for +database administrators with a lot of experience in SQL and little +experience in Python, since its basic mode of operation is just to run +plain old SQL scripts (hence the name). + +Its design guidelines are the following: + +- conciseness + +sqlplain is committed to keep the codebase small (say under 2000 lines) +and the number of features limited; + +- simplicity + +sqlplain is based on a simple +implementation, to make it easy for its users to understand it and to +build the features they need; + +- easyness of use + +sqlplain has a small learning curve +and provides a small set of convenient utilities for scripting your +database interation; + +- familiarity + +sqlplain connections are a simplified version of standard +DB API 2 connections, and you can readily get the underlying low +level connection to use of all its power, if need there is; + +- transparency + +sqlplain tries hard not to add too many layers of indirection to your +SQL queries: the idea is that it should be easy for the user to extract +the plain old SQL query from the code, cut and paste it on the database +console, and try it out directly. + +sqlplain can be used to do anything on a database, but there is special +support for the following areas: + +- creating test databases +- running database performance tests +- introspecting pre-existing databases +- scripting database operations +- memoizing queries + +You can use sqlplain with your Web framework of choice (I have +been running it with Pylons for years without problems) but there +is no special integration with any framework - this is an advantage +or a disadvantage depending on your point of view. Certainly sqlplain +is *not* intended to be used as the Django ORM. Django (as most +Python ORMs) puts the emphasis on Python, in the sense that the +model is written down as a set of Python classes which are then +persistent to the database. On the contrary, sqlplain assumes +that you already have your tables on the database and that they +are managed via DDL queries: you can introspect your tables +from Python, but if you want to look at their definitions you +must look into SQL code, not into Python code. There is no +facility to create tables from Python code (actually there is, +but it works by just passing plain SQL code to the creational procedure) +and no facility to modify the schema of an existing table from +Python (apart for passing raw "ALTER TABLE" queries to the connection +object). + +sqlplain does provide limited support for the following areas: + +- database independence + +True database-independence is a dream, since the features and the +performance characteristics provided by different databases are +totally different. Therefore, sqlplain does not even attempt to +be a fully database-independent toolkit. However, in the cases where +I could keep database-independence with little effort, I kept it. +In particular the introspection functionalities are database-independent. +But in general I judge spending a lot of time to provide a false sensation +of database independence not worth the effort. + +- programmatic generation of SQL queries + +Some database toolkits (notably SQLAlchemy) have an incredibly sophisticated +mechanism to generate SQL queries from Python objects in a +database-independent way. That is not the goal of +sqlplain. Out of the box sqlplain provides no support at all for generating +SQL queries programmatically, however the documentation provides a few +do-it-yourself recipes. + +- Object Relation Mapper + +The core of sqlplain does not provide any ORM. A small table framework +is provided as an experimental extension: its goal is to provide a small +object oriented wrapper over the tables of database, but it is not an +ORM such as the ones you can find in SQLAlchemy, SQLObject or Storm. +It is pretty easy to write custom classes making use of the table +framework, but you are on your own, there is nothing built-in. +This is done on purpose. + +- Connection pooling + +sqlplain does not provide any connection pooling facility and it will never +provide one. There are already many connection pool implementations available +out there and just use the one you like, if you need one. +Notice that you can use sqlplain connections in a multithreaded application +(such as a Web server) even without an explicit connection pool since the +connections can be made thread local. + +- no support for nested transactions, no unit-of-work pattern. + + + + diff --git a/sqlplain/doc/logtable.py b/sqlplain/doc/logtable.py new file mode 100644 index 0000000..9928ccd --- /dev/null +++ b/sqlplain/doc/logtable.py @@ -0,0 +1,14 @@ +from datetime import datetime +from sqlplain import do, lazyconnect, util, table + +create_log_table = do(""" +CREATE TABLE log( + date DATETIME, + message VARCHAR(255)) +""") + +def init(db_uri): + db = lazyconnect(db_uri) + util.drop_table(db, 'log', force=True) + create_log_table(db) + return db diff --git a/sqlplain/doc/logtable.py~ b/sqlplain/doc/logtable.py~ new file mode 100644 index 0000000..50713a8 --- /dev/null +++ b/sqlplain/doc/logtable.py~ @@ -0,0 +1,12 @@ +from sqlplain import do, lazyconnect, util + +create_log_table = do(""" +CREATE TABLE log( + date DATETIME, + message VARCHAR(255)) +""") + +if __name__ == '__main__': + db = lazyconnect('sqlite_test') + util.drop_table(db, 'log', force=True) + create_log_table(db) diff --git a/sqlplain/doc/queries.py~ b/sqlplain/doc/queries.py~ new file mode 100644 index 0000000..e3f0dd9 --- /dev/null +++ b/sqlplain/doc/queries.py~ @@ -0,0 +1,5 @@ +from sqlplain import do + +get_authors = do('select author from book') +get_titles = do('select title from book where author=?') +set_uppercase_titles = do('UPDATE book SET title=upper(title)') diff --git a/sqlplain/doc/tables.py b/sqlplain/doc/tables.py new file mode 100644 index 0000000..470ffa7 --- /dev/null +++ b/sqlplain/doc/tables.py @@ -0,0 +1,90 @@ +""" +The table framework +------------------------------------------------------------ + +As I said in the introduction, ``sqlplain`` is not intended to be +a fully fledged ORM, therefore it does not provide a builtin mechanism +to map user defined classes to database objects, such as the mapping +mechanism in SQLAlchemy, or the popular Active Record pattern; nevertheless, +it provides a table framework which us a lightweight object oriented layer +over database tables. +``sqlplain`` table object comes in two flavors: D-tables, which should be +used for tables without a primary key, and K-tables, which must +used for tables with a primary key, possibly composite. Actually K-tables +are D-tables too, since they inherit from D-tables, therefore they +share all D-tables methods and they add a few methods which are +meaninful only if the underlying table has a primary key. +If you try to use a K-table over a database table which has no primary +key, a ``TypeError`` will be raised. Here is an example: + +$$logtable + +>>> from sqlplain import table +>>> import logtable +>>> db = logtable.init('sqlite_test') +>>> log = table.KTable.reflect(db, 'log') +Traceback (most recent call last): + ... +TypeError: table log has no primary key! + +Using a DTable instead works: + +>>> log = table.DTable.reflect(db, 'log') + +The ``.reflect`` classmethod creates a suitable subclass of ``DTable`` +(called ``Log``) and instantiates it. ``DTable`` is a kind of abstract +base class and you cannot instantiate it directly (the same is true +for the KTable class): + +>>> table.DTable(db) +Traceback (most recent call last): + ... +TypeError: You cannot instantiate the ABC DTable + +In order to create a concrete subclass of DTable (or KTable) one needs +to set the tabletuple class attribute ``tt``, which contains information +about the table structure. The ``.reflect`` method extracts the information +from the database schema; for instance ``log.tt`` is a namedtuple with +fields ``date`` and ``message``: + +>>> print log.tt._fields +('date', 'message') + + +>>> from datetime import datetime +>>> now = datetime.now +>>> log.insert_row(date=now(), message='message1') +1 +>>> log.insert_row(date=now(), message='message2') +1 +>>> print len(log) +2 + +Here is the full API for DTable: + +- create +- select +- count +- delete +- truncate +- insert_row +- insert_rows +- insert_file + +Here are the additional functions of K-tables: + +- select_row +- update_row +- update_or_insert_row +- delete_row + +Instead of an explanation, I will give examples:: + + select_book = select_row('book', 'title author') + select_book(bookdb, title='T', author='A') + +``select_row`` raises an error if the corresponding queries +returns no result (you are looking for a missing record) or +if it returns multiple results (it means that your primary key specification +was incomplete). +""" diff --git a/sqlplain/doc/tables.py~ b/sqlplain/doc/tables.py~ new file mode 100644 index 0000000..6f3f401 --- /dev/null +++ b/sqlplain/doc/tables.py~ @@ -0,0 +1,92 @@ +""" +The table framework +------------------------------------------------------------ + +As I said in the introduction, ``sqlplain`` is not intended to be +a fully fledged ORM, therefore it does not provide a builtin mechanism +to map user defined classes to database objects, such as the mapping +mechanism in SQLAlchemy, or the popular Active Record pattern; nevertheless, +it provides a table framework which us a lightweight object oriented layer +over database tables. +``sqlplain`` table object comes in two flavors: D-tables, which should be +used for tables without a primary key, and K-tables, which must +used for tables with a primary key, possibly composite. Actually K-tables +are D-tables too, since they inherit from D-tables, therefore they +share all D-tables methods and they add a few methods which are +meaninful only if the underlying table has a primary key. +If you try to use a K-table over a database table which has no primary +key, a ``TypeError`` will be raised. Here is an example: + +$$logtable + +>>> from sqlplain import table +>>> import logtable +>>> db = logtable.init('sqlite_test') +>>> log = table.KTable.reflect(db, 'log') +Traceback (most recent call last): + ... +TypeError: table log has no primary key! + +Using a DTable instead works: + +>>> log = table.DTable.reflect(db, 'log') + +The ``.reflect`` classmethod creates a suitable subclass of ``DTable`` +(called ``Log``) and instantiates it. ``DTable`` is a kind of abstract +base class and you cannot instantiate it directly (the same is true +for the KTable class): + +>>> table.DTable(db) +Traceback (most recent call last): + ... +TypeError: You cannot instantiate the ABC DTable + +In order to create a concrete subclass of DTable (or KTable) one needs +to set the tabletuple class attribute ``tt``, which contains information +about the table structure. The ``.reflect`` method extracts the information +from the database schema; for instance ``log.tt`` is a namedtuple with +fields ``date`` and ``message``: + +>>> print log.tt._fields +('date', 'message') + + +>>> from datetime import datetime +>>> now = datetime.now +>>> log.insert_row(date=now(), message='message1') +1 +>>> log.insert_row(date=now(), message='message2') +1 +>>> print len(log) +2 + +Here is the full API for DTable: + +- type +- create +- reflect +- select +- count +- delete +- truncate +- insert_row +- insert_rows +- insert_file + +Here are the additional methods of K-tables: + +- select_row +- update_row +- update_or_insert_row +- delete_row + +Instead of an explanation, I will give examples:: + + select_book = select_row('book', 'title author') + select_book(bookdb, title='T', author='A') + +``select_row`` raises an error if the corresponding queries +returns no result (you are looking for a missing record) or +if it returns multiple results (it means that your primary key specification +was incomplete). +""" diff --git a/sqlplain/mssql_util.py b/sqlplain/mssql_util.py index baad580..da6b865 100644 --- a/sqlplain/mssql_util.py +++ b/sqlplain/mssql_util.py @@ -7,6 +7,9 @@ def insert_file_mssql(uri, fname, table, sep='\t'): return conn.execute( 'BULK INSERT %s FROM ? SEPARATOR ?' % table, (fname, sep)) +def get_tables(conn): + return [r.name for r in conn.execute('SELECT name FROM sysobjects')] + def exists_table_mssql(conn, tname): return conn.execute('SELECT count(*) FROM sysobjects WHERE name=?', (tname,), scalar=True) diff --git a/sqlplain/postgres_util.py b/sqlplain/postgres_util.py index 9045791..a1fe5a4 100644 --- a/sqlplain/postgres_util.py +++ b/sqlplain/postgres_util.py @@ -21,6 +21,10 @@ def drop_db_postgres(uri): openclose(uri.copy(database='template1'), 'DROP DATABASE %(database)s' % uri) + +def get_tables_postgres(conn): + return [r.name for r in conn.execute('SELECT name FROM pg_tables')] + def exists_table_postgres(conn, tname): return conn.execute('SELECT count(*) FROM pg_tables WHERE name=?', tname, scalar=True) diff --git a/sqlplain/sqlite_util.py b/sqlplain/sqlite_util.py index b304814..0c1d33f 100644 --- a/sqlplain/sqlite_util.py +++ b/sqlplain/sqlite_util.py @@ -21,6 +21,9 @@ def insert_file_sqlite(conn, fname, tname, sep): def get_kfields_sqlite(conn, tname): return [x.name for x in get_info(conn, tname) if x.pk] +def get_tables_sqlite(conn): + return [r.name for r in conn.execute('PRAGMA table_info')] + def exists_table_sqlite(conn, tname): res = conn.execute('PRAGMA table_info(%s)' % tname) return res != -1 diff --git a/sqlplain/table.py b/sqlplain/table.py index e6d13c0..7e50d29 100644 --- a/sqlplain/table.py +++ b/sqlplain/table.py @@ -1,8 +1,6 @@ import sys, string from sqlplain import util, do from sqlplain.namedtuple import namedtuple -from sqlplain.connection import connmethod - # kfields and dfields must be tuples, not strings def tabletuple(name, kfields, dfields): @@ -21,8 +19,7 @@ def tabletuple(name, kfields, dfields): lambda self: dtuple(*[getattr(self, n) for n in self._dfields])) return ttuple -# note: the methods of the Table classes -# are completely external to the class except for the .conn attribute +# closures to be instantiated in DTable.__init__ def insert(ttuple): "Return a procedure inserting a row or a dictionary into a table" @@ -114,76 +111,77 @@ def update_or_insert(ttuple): return n update_or_insert_row.__doc__ = update_or_insert_row.templ = None return update_or_insert_row - -class DTable(object): + +class DView(object): """ - A simple table class for database tables without a primary key. - The only methods are insert_row, insert_file, delete, truncate, select. """ - _registry = {} - - @classmethod - def type(cls, name, fields): - "Ex. Insert = DTable.type('book', 'serial', 'title author')" - fields = tuple(fields) - if (name, fields) in cls._registry: - return cls._registry[name, fields] - tt = namedtuple(name, fields) - def insert_file(conn, file, sep='\t'): - 'Populate a table by reading a file-like object' - return util.insert_file(conn, file, name, sep) - def insert_rows(conn, rows): - 'Populate a table by reading a row-iterator' - return util.insert_rows(conn, name, rows) - dic = dict( - tt = tt, - name = tt.__name__, - insert_row = connmethod(insert(tt)), - insert_rows = connmethod(insert_rows), - insert_file = connmethod(insert_file), - _registry = {}, - ) - subc = type(name.capitalize(), (cls,), dic) - cls._registry[name, fields] = subc - return subc - - @classmethod - def reflect(cls, conn, name): - "Ex. insert = DTable.reflect(mydb, 'book')" - fields = util.get_fields(conn, name) - return cls.type(name, fields)(conn) @classmethod def create(cls, conn, name, fields, force=False): - util.create_table(conn, name, fields, force) - return cls.reflect(conn, name) - - @connmethod - def insert_row(conn, row, **kw): - "Dynamically replaced in subclasses" - - @connmethod - def insert_rows(conn, row, **kw): - "Dynamically replaced in subclasses" - - @connmethod - def insert_file(conn, row, **kw): - "Dynamically replaced in subclasses" + util.create_view(conn, name, fields, force) + return cls(conn, name) - def __init__(self, conn): - if self.__class__ in (DTable, KTable): - raise TypeError('You cannot instantiate the ABC %s' % - self.__class__.__name__) - self.tt # raise an AttributeError if not set correctly + def __init__(self, conn, name, fields=(), query=''): self.conn = conn + self.name = name + if query: + self.query = '(%s) AS %s' % (query, name) + s = 'SELECT * FROM %s WHERE 1=0' % self.query + fields = [r.name for name in conn.execute(s).descr] + else: + self.query = name + fields = fields or util.get_fields(conn, name) + self.tt = tabletuple(name, fields) def select(self, clause='', *args): "Select rows from the table" fields = ', '.join(self.tt._fields) - templ = 'SELECT %s FROM %s %s' % (fields, self.name, clause) + templ = 'SELECT %s FROM %s %s' % (fields, self.query, clause) if args: return do(templ, ntuple=self.tt)(self.conn, templ, *args) - return self.conn.execute(templ, ntuple=self.tt) + else: + return self.conn.execute(templ, ntuple=self.tt) + + def count(self, clause=''): + "Count the number of rows satisfying the given clause" + templ = 'SELECT COUNT(*) FROM %s %s' % (self.query, clause) + if args: + return do(templ)(self.conn, templ, *args) + return self.conn.execute(templ, scalar=True) + + def __iter__(self): + return self.select() + + def __len__(self): + "Return the total number of rows in the table" + return self.count() + +class DTable(DView): + """ + A simple table class for database tables without a primary key. + The only methods are insert_row, insert_file, delete, truncate, select. + """ + + @classmethod + def create(cls, conn, name, fields, force=False): + util.create_table(conn, name, fields, force) + return cls(conn, name, fields) + + def insert_rows(self, rows): + 'Populate a table by reading a row-iterator' + return util.insert_rows(self.conn, self.name, rows) + + def insert_file(self, file, sep='1t'): + 'Populate a table by reading a file-like object' + return util.insert_file(self.conn, file, self.name, sep) + + def __init__(self, conn, name, fields=()): + self.conn = conn + self.name = self.query = name + if not fields: + fields = util.get_fields(conn, name) + self.tt = namedtuple(name, fields) + self.insert_row = insert(self.tt) def delete(self, clause=''): "Delete rows from the table" @@ -195,49 +193,22 @@ class DTable(object): def truncate(self): "Truncate the table" return util.truncate_table(self.conn, self.name) - - def count(self, clause=''): - "Count the number of rows satisfying the given clause" - templ = 'SELECT COUNT(*) FROM %s %s' % (self.name, clause) - if args: - return do(templ)(self.conn, templ, *args) - return self.conn.execute(templ, scalar=True) - - def __len__(self): - "Return the total number of rows in the table" - return self.count() class KTable(DTable): """ An object oriented wrapper for database tables with a primary key. """ - _registry = {} - @classmethod - def type(cls, name, kfields, dfields): + def __init__(cls, name, kfields=(), dfields=()): "Ex. Book = KTable.type('book', 'serial', 'title author')" + kfields = kfields or util.get_kfields(conn, name) + dfields = dfields or util.get_dfields(conn, name) if not kfields: raise TypeError('table %s has no primary key!' % name) - kfields = tuple(kfields) - dfields = tuple(dfields) - if (name, kfields, dfields) in cls._registry: - return cls._registry[name, kfields, dfields] - tt = tabletuple(name, kfields, dfields) - d = dict(tt=tt, name=name, _registry = {}) + self.tt = tabletuple(name, kfields, dfields) for nam in ('insert', 'delete', 'select', 'update', 'update_or_insert'): - func = globals()[nam](tt) - cmethod = connmethod(func) - d[nam + '_row'] = cmethod - subc = type(name.capitalize(), (cls,), d) - cls._registry[name, kfields, dfields] = subc - return subc - - @classmethod - def reflect(cls, conn, name): - "Ex. book = KTable.reflect(mydb, 'book')" - kfields = util.get_kfields(conn, name) - dfields = util.get_dfields(conn, name) - return cls.type(name, kfields, dfields)(conn) + func = globals()[nam](self.tt) + setattr(self, nam + '_row', func) def __contains__(self, key): try: diff --git a/sqlplain/util.py b/sqlplain/util.py index a2ac214..910744a 100644 --- a/sqlplain/util.py +++ b/sqlplain/util.py @@ -4,7 +4,8 @@ Notice: create_db and drop_db are not transactional. import os, sys, re from sqlplain.uri import URI -from sqlplain import lazyconnect, Transaction, do +from sqlplain import lazyconnect, do +from sqlplain.connection import Transaction from sqlplain.namedtuple import namedtuple VERSION = re.compile(r'(\d[\d\.-]+)') @@ -43,7 +44,7 @@ def drop_db(uri): "Drop an existing database" _call('drop_db', URI(uri)) -# helper for createdb +# helper for create_db def _collect(directory, exts): ''' Read the files with a given set of extensions from a directory @@ -139,7 +140,7 @@ def insert_rows(conn, tname, rows): n = conn.execute(templ, row) for row in it: n += conn.execute(templ, row) - return n + return def insert_file(conn, fname, tname, sep=','): "Bulk insert a CSV file into a table""" @@ -147,6 +148,10 @@ def insert_file(conn, fname, tname, sep=','): ########################## introspection routines ###################### +def get_tables(conn): + "Return the names of the tables in the current database" + return _call('get_tables', conn) + def exists_table(conn, tname): "Check if a table exists" return _call('exists_table', conn, tname) |