228 lines
7.4 KiB
Python
228 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# gPodder - A media aggregator and podcast client
|
|
# Copyright (c) 2005-2018 The gPodder Team
|
|
#
|
|
# gPodder is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# gPodder is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# gpodder.minidb - A simple SQLite store for Python objects
|
|
# Thomas Perl, 2010-01-28
|
|
|
|
# based on: "ORM wie eine Kirchenmaus - a very poor ORM implementation
|
|
# by thp, 2009-11-29 (thp.io/about)"
|
|
|
|
# This module is also available separately at:
|
|
# http://thp.io/2010/minidb/
|
|
|
|
|
|
# For Python 2.5, we need to request the "with" statement
|
|
|
|
|
|
try:
|
|
import sqlite3.dbapi2 as sqlite
|
|
except ImportError:
|
|
try:
|
|
from pysqlite2 import dbapi2 as sqlite
|
|
except ImportError:
|
|
raise Exception('Please install SQLite3 support.')
|
|
|
|
|
|
import threading
|
|
|
|
|
|
class Store(object):
|
|
def __init__(self, filename=':memory:'):
|
|
self.db = sqlite.connect(filename, check_same_thread=False)
|
|
self.lock = threading.RLock()
|
|
|
|
def _schema(self, class_):
|
|
return class_.__name__, list(sorted(class_.__slots__))
|
|
|
|
def _set(self, o, slot, value):
|
|
# Set a slot on the given object to value, doing a cast if
|
|
# necessary. The value None is special-cased and never cast.
|
|
cls = o.__class__.__slots__[slot]
|
|
if value is not None:
|
|
if isinstance(value, bytes):
|
|
value = value.decode('utf-8')
|
|
value = cls(value)
|
|
setattr(o, slot, value)
|
|
|
|
def commit(self):
|
|
with self.lock:
|
|
self.db.commit()
|
|
|
|
def close(self):
|
|
with self.lock:
|
|
self.db.isolation_level = None
|
|
self.db.execute('VACUUM')
|
|
self.db.isolation_level = ''
|
|
self.db.close()
|
|
|
|
def _register(self, class_):
|
|
with self.lock:
|
|
table, slots = self._schema(class_)
|
|
cur = self.db.execute('PRAGMA table_info(%s)' % table)
|
|
available = cur.fetchall()
|
|
|
|
if available:
|
|
available = [row[1] for row in available]
|
|
missing_slots = (s for s in slots if s not in available)
|
|
for slot in missing_slots:
|
|
self.db.execute('ALTER TABLE %s ADD COLUMN %s TEXT' % (table,
|
|
slot))
|
|
else:
|
|
self.db.execute('CREATE TABLE %s (%s)' % (table,
|
|
', '.join('%s TEXT' % s for s in slots)))
|
|
|
|
def convert(self, v):
|
|
if isinstance(v, str):
|
|
return v
|
|
elif isinstance(v, str):
|
|
# XXX: Rewrite ^^^ as "isinstance(v, bytes)" in Python 3
|
|
return v.decode('utf-8')
|
|
else:
|
|
return str(v)
|
|
|
|
def update(self, o, **kwargs):
|
|
self.remove(o)
|
|
for k, v in list(kwargs.items()):
|
|
setattr(o, k, v)
|
|
self.save(o)
|
|
|
|
def save(self, o):
|
|
if hasattr(o, '__iter__'):
|
|
klass = None
|
|
for child in o:
|
|
if klass is None:
|
|
klass = child.__class__
|
|
self._register(klass)
|
|
table, slots = self._schema(klass)
|
|
|
|
if not isinstance(child, klass):
|
|
raise ValueError('Only one type of object allowed')
|
|
|
|
used = [s for s in slots if getattr(child, s, None) is not None]
|
|
values = [self.convert(getattr(child, slot)) for slot in used]
|
|
self.db.execute('INSERT INTO %s (%s) VALUES (%s)' % (table,
|
|
', '.join(used), ', '.join('?' * len(used))), values)
|
|
return
|
|
|
|
with self.lock:
|
|
self._register(o.__class__)
|
|
table, slots = self._schema(o.__class__)
|
|
|
|
values = [self.convert(getattr(o, slot)) for slot in slots]
|
|
self.db.execute('INSERT INTO %s (%s) VALUES (%s)' % (table,
|
|
', '.join(slots), ', '.join('?' * len(slots))), values)
|
|
|
|
def delete(self, class_, **kwargs):
|
|
with self.lock:
|
|
self._register(class_)
|
|
table, slots = self._schema(class_)
|
|
sql = 'DELETE FROM %s' % (table,)
|
|
if kwargs:
|
|
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
|
try:
|
|
self.db.execute(sql, list(kwargs.values()))
|
|
return True
|
|
except Exception as e:
|
|
return False
|
|
|
|
def remove(self, o):
|
|
if hasattr(o, '__iter__'):
|
|
for child in o:
|
|
self.remove(child)
|
|
return
|
|
|
|
with self.lock:
|
|
self._register(o.__class__)
|
|
table, slots = self._schema(o.__class__)
|
|
|
|
# Use "None" as wildcard selector in remove actions
|
|
slots = [s for s in slots if getattr(o, s, None) is not None]
|
|
|
|
values = [self.convert(getattr(o, slot)) for slot in slots]
|
|
self.db.execute('DELETE FROM %s WHERE %s' % (table,
|
|
' AND '.join('%s=?' % s for s in slots)), values)
|
|
|
|
def load(self, class_, **kwargs):
|
|
with self.lock:
|
|
self._register(class_)
|
|
table, slots = self._schema(class_)
|
|
sql = 'SELECT %s FROM %s' % (', '.join(slots), table)
|
|
if kwargs:
|
|
sql += ' WHERE %s' % (' AND '.join('%s=?' % k for k in kwargs))
|
|
try:
|
|
cur = self.db.execute(sql, list(kwargs.values()))
|
|
except Exception as e:
|
|
raise
|
|
|
|
def apply(row):
|
|
o = class_.__new__(class_)
|
|
for attr, value in zip(slots, row):
|
|
try:
|
|
self._set(o, attr, value)
|
|
except ValueError as ve:
|
|
return None
|
|
return o
|
|
return [x for x in [apply(row) for row in cur] if x is not None]
|
|
|
|
def get(self, class_, **kwargs):
|
|
result = self.load(class_, **kwargs)
|
|
if result:
|
|
return result[0]
|
|
else:
|
|
return None
|
|
|
|
|
|
if __name__ == '__main__':
|
|
class Person(object):
|
|
__slots__ = {'username': str, 'id': int}
|
|
|
|
def __init__(self, username, id):
|
|
self.username = username
|
|
self.id = id
|
|
|
|
def __repr__(self):
|
|
return '<Person "%s" (%d)>' % (self.username, self.id)
|
|
|
|
m = Store()
|
|
m.save(Person('User %d' % x, x * 20) for x in range(50))
|
|
|
|
p = m.get(Person, id=200)
|
|
print(p)
|
|
m.remove(p)
|
|
p = m.get(Person, id=200)
|
|
|
|
# Remove some persons again (deletion by value!)
|
|
m.remove(Person('User %d' % x, x * 20) for x in range(40))
|
|
|
|
class Person(object):
|
|
__slots__ = {'username': str, 'id': int, 'mail': str}
|
|
|
|
def __init__(self, username, id, mail):
|
|
self.username = username
|
|
self.id = id
|
|
self.mail = mail
|
|
|
|
def __repr__(self):
|
|
return '<Person "%s" (%s)>' % (self.username, self.mail)
|
|
|
|
# A schema update takes place here
|
|
m.save(Person('User %d' % x, x * 20, 'user@home.com') for x in range(50))
|
|
print(m.load(Person))
|