Fix bug in text edit dialog (no response)
[gpodder.git] / src / gpodder / minidb.py
bloba9dad282cfb47a69993efb6b5b42748907053f6c
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # gPodder - A media aggregator and podcast client
5 # Copyright (c) 2005-2010 Thomas Perl and the gPodder Team
7 # gPodder is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # gPodder is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 # gpodder.minidb - A simple SQLite store for Python objects
22 # Thomas Perl, 2010-01-28
24 # based on: "ORM wie eine Kirchenmaus - a very poor ORM implementation
25 # by thp, 2009-11-29 (thpinfo.com/about)"
28 # For Python 2.5, we need to request the "with" statement
29 from __future__ import with_statement
31 import sys
33 try:
34 import sqlite3.dbapi2 as sqlite
35 except ImportError:
36 try:
37 from pysqlite2 import dbapi2 as sqlite
38 except ImportError:
39 raise Exception('Please install SQLite3 support.')
42 import threading
44 class Store(object):
45 def __init__(self, filename=':memory:'):
46 self.db = sqlite.connect(filename, check_same_thread=False)
47 self.lock = threading.RLock()
49 def _schema(self, class_):
50 return class_.__name__, list(sorted(class_.__slots__))
52 def _set(self, o, slot, value):
53 # Set a slot on the given object to value, doing a cast if
54 # necessary. The value None is special-cased and never cast.
55 if value is not None:
56 value = o.__class__.__slots__[slot](value)
57 setattr(o, slot, value)
59 def commit(self):
60 with self.lock:
61 self.db.commit()
63 def close(self):
64 with self.lock:
65 self.db.close()
67 def _register(self, class_):
68 with self.lock:
69 table, slots = self._schema(class_)
70 cur = self.db.execute('PRAGMA table_info(%s)' % table)
71 available = cur.fetchall()
73 if available:
74 available = [row[1] for row in available]
75 missing_slots = (s for s in slots if s not in available)
76 for slot in missing_slots:
77 self.db.execute('ALTER TABLE %s ADD COLUMN %s TEXT' % (table,
78 slot))
79 else:
80 self.db.execute('CREATE TABLE %s (%s)' % (table,
81 ', '.join('%s TEXT'%s for s in slots)))
83 def update(self, o, **kwargs):
84 self.remove(o)
85 for k, v in kwargs.items():
86 setattr(o, k, v)
87 self.save(o)
89 def save(self, o):
90 if hasattr(o, '__iter__'):
91 for child in o:
92 self.save(child)
93 return
95 with self.lock:
96 self._register(o.__class__)
97 table, slots = self._schema(o.__class__)
99 # Only save values that have values set (non-None values)
100 slots = [s for s in slots if getattr(o, s, None) is not None]
102 values = [str(getattr(o, slot)) for slot in slots]
103 self.db.execute('INSERT INTO %s (%s) VALUES (%s)' % (table,
104 ', '.join(slots), ', '.join('?'*len(slots))), values)
106 def remove(self, o):
107 if hasattr(o, '__iter__'):
108 for child in o:
109 self.remove(child)
110 return
112 with self.lock:
113 self._register(o.__class__)
114 table, slots = self._schema(o.__class__)
116 # Use "None" as wildcard selector in remove actions
117 slots = [s for s in slots if getattr(o, s, None) is not None]
119 values = [getattr(o, slot) for slot in slots]
120 self.db.execute('DELETE FROM %s WHERE %s' % (table,
121 ' AND '.join('%s=?'%s for s in slots)), values)
123 def load(self, class_, **kwargs):
124 with self.lock:
125 self._register(class_)
126 table, slots = self._schema(class_)
127 sql = 'SELECT %s FROM %s' % (', '.join(slots), table)
128 if kwargs:
129 sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
130 try:
131 cur = self.db.execute(sql, kwargs.values())
132 except Exception, e:
133 print sql
134 raise
135 def apply(row):
136 o = class_.__new__(class_)
137 for attr, value in zip(slots, row):
138 self._set(o, attr, value)
139 return o
140 return [apply(row) for row in cur]
142 def get(self, class_, **kwargs):
143 result = self.load(class_, **kwargs)
144 if result:
145 return result[0]
146 else:
147 return None
149 if __name__ == '__main__':
150 class Person(object):
151 __slots__ = {'username': str, 'id': int}
153 def __init__(self, username, id):
154 self.username = username
155 self.id = id
157 def __repr__(self):
158 return '<Person "%s" (%d)>' % (self.username, self.id)
160 m = Store()
161 m.save(Person('User %d' % x, x*20) for x in range(50))
163 p = m.get(Person, id=200)
164 print p
165 m.remove(p)
166 p = m.get(Person, id=200)
168 # Remove some persons again (deletion by value!)
169 m.remove(Person('User %d' % x, x*20) for x in range(40))
171 class Person(object):
172 __slots__ = {'username': str, 'id': int, 'mail': str}
174 def __init__(self, username, id, mail):
175 self.username = username
176 self.id = id
177 self.mail = mail
179 def __repr__(self):
180 return '<Person "%s" (%s)>' % (self.username, self.mail)
182 # A schema update takes place here
183 m.save(Person('User %d' % x, x*20, 'user@home.com') for x in range(50))
184 print m.load(Person)