2 # -*- coding: utf-8 -*-
4 # minidb - A simple SQLite3 store for Python objects
5 # (based on "ORM wie eine Kirchenmaus" by thp, 2009-11-29)
7 # Copyright 2009-2010 Thomas Perl <thp.io>. All rights reserved.
9 # Permission to use, copy, modify, and/or distribute this software for any
10 # purpose with or without fee is hereby granted, provided that the above
11 # copyright notice and this permission notice appear in all copies.
13 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
14 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
15 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
16 # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
17 # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
18 # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
19 # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22 """A simple SQLite3 store for Python objects"""
24 # For Python 2.5, we need to request the "with" statement
25 from __future__
import with_statement
27 __author__
= 'Thomas Perl <m@thp.io>'
29 __website__
= 'http://thp.io/2010/minidb/'
33 import sqlite3
.dbapi2
as sqlite
36 from pysqlite2
import dbapi2
as sqlite
38 raise Exception('Please install SQLite3 support.')
44 def __init__(self
, filename
=':memory:'):
45 """Create (or load) a new minidb storage
47 Without arguments, this will create an in-memory
48 database that will be deleted when closed. If you
49 pass an argument, it should be the filename of the
50 database file (which will be created if it does
53 self
.db
= sqlite
.connect(filename
, check_same_thread
=False)
54 self
.lock
= threading
.RLock()
56 def _schema(self
, class_
):
57 return class_
.__name
__, list(sorted(class_
.__slots
__))
59 def _set(self
, o
, slot
, value
):
60 # Set a slot on the given object to value, doing a cast if
61 # necessary. The value None is special-cased and never cast.
62 cls
= o
.__class
__.__slots
__[slot
]
64 if isinstance(value
, unicode):
65 value
= value
.decode('utf-8')
67 setattr(o
, slot
, value
)
70 """Commit changes into the database"""
75 """Close the underlying database file"""
79 def _register(self
, class_
):
81 table
, slots
= self
._schema
(class_
)
82 cur
= self
.db
.execute('PRAGMA table_info(%s)' % table
)
83 available
= cur
.fetchall()
86 available
= [row
[1] for row
in available
]
87 missing_slots
= (s
for s
in slots
if s
not in available
)
88 for slot
in missing_slots
:
89 self
.db
.execute('ALTER TABLE %s ADD COLUMN %s TEXT' % (table
,
92 self
.db
.execute('CREATE TABLE %s (%s)' % (table
,
93 ', '.join('%s TEXT'%s for s
in slots
)))
96 """Convert a value to its string representation"""
97 if isinstance(v
, str) or isinstance(v
, unicode):
102 def update(self
, o
, **kwargs
):
103 """Update fields of an object and store the changes
105 This will update named fields (specified by keyword
106 arguments) inside the object and also store these
107 changes in the database.
110 for k
, v
in kwargs
.items():
115 """Save an object into the database
117 Save a newly-created object into the database. The
118 object will always be newly created, never updated.
120 If you want to update an object inside the database,
121 please use the "update" method instead.
123 if hasattr(o
, '__iter__'):
129 self
._register
(o
.__class
__)
130 table
, slots
= self
._schema
(o
.__class
__)
132 # Only save values that have values set (non-None values)
133 slots
= [s
for s
in slots
if getattr(o
, s
, None) is not None]
135 values
= [self
.convert(getattr(o
, slot
)) for slot
in slots
]
136 self
.db
.execute('INSERT INTO %s (%s) VALUES (%s)' % (table
,
137 ', '.join(slots
), ', '.join('?'*len(slots
))), values
)
139 def delete(self
, class_
, **kwargs
):
140 """Delete objects from the database
142 Delete objects of type "class_" with the criteria
143 specified in "kwargs". Please note that all objects
144 that match the criteria will be deleted.
146 If you want to remove a specific object from the
147 database, use "remove" instead.
150 self
._register
(class_
)
151 table
, slots
= self
._schema
(class_
)
152 sql
= 'DELETE FROM %s' % (table
,)
154 sql
+= ' WHERE %s' % (' AND '.join('%s=?' % k
for k
in kwargs
))
156 self
.db
.execute(sql
, kwargs
.values())
162 """Delete objects by template object
164 This will remove all objects from the database that
165 compare to the given object (i.e. all attributes of
166 "o" that are not None will match to the objects in
169 This method should be used to remove specific object
170 only. For bulk deletion based on some criteria, the
171 "delete" method might be better suited.
173 if hasattr(o
, '__iter__'):
179 self
._register
(o
.__class
__)
180 table
, slots
= self
._schema
(o
.__class
__)
182 # Use "None" as wildcard selector in remove actions
183 slots
= [s
for s
in slots
if getattr(o
, s
, None) is not None]
185 values
= [self
.convert(getattr(o
, slot
)) for slot
in slots
]
186 self
.db
.execute('DELETE FROM %s WHERE %s' % (table
,
187 ' AND '.join('%s=?'%s for s
in slots
)), values
)
189 def load(self
, class_
, **kwargs
):
190 """Load objects of a given class
192 Return a list of objects from the database that are of
193 type "class_". By default, all objects are returned,
194 but a simple pre-selection can be made using keyword
198 self
._register
(class_
)
199 table
, slots
= self
._schema
(class_
)
200 sql
= 'SELECT %s FROM %s' % (', '.join(slots
), table
)
202 sql
+= ' WHERE %s' % (' AND '.join('%s=?' % k
for k
in kwargs
))
204 cur
= self
.db
.execute(sql
, kwargs
.values())
208 o
= class_
.__new
__(class_
)
209 for attr
, value
in zip(slots
, row
):
211 self
._set
(o
, attr
, value
)
212 except ValueError, ve
:
215 return filter(lambda x
: x
is not None, [apply(row
) for row
in cur
])
217 def get(self
, class_
, **kwargs
):
218 """Load one object of a given class
220 This is a convenience function that will load only a
221 single object from the database, returning only that
222 object or None when the object is not found.
224 This method only makes sense when using keyword
225 arguments to select the object (i.e. using a
226 unique set of attributes to retrieve it).
228 result
= self
.load(class_
, **kwargs
)
234 if __name__
== '__main__':
235 class Person(object):
236 __slots__
= {'username': str, 'id': int}
238 def __init__(self
, username
, id):
239 self
.username
= username
243 return '<Person "%s" (%d)>' % (self
.username
, self
.id)
246 m
.save(Person('User %d' % x
, x
*20) for x
in range(50))
248 p
= m
.get(Person
, id=200)
251 p
= m
.get(Person
, id=200)
253 # Remove some persons again (deletion by value!)
254 m
.remove(Person('User %d' % x
, x
*20) for x
in range(40))
256 class Person(object):
257 __slots__
= {'username': str, 'id': int, 'mail': str}
259 def __init__(self
, username
, id, mail
):
260 self
.username
= username
265 return '<Person "%s" (%s)>' % (self
.username
, self
.mail
)
267 # A schema update takes place here
268 m
.save(Person('User %d' % x
, x
*20, 'user@home.com') for x
in range(50))