#!/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 . # # 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 '' % (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 '' % (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))