1# -*- coding: utf-8 -*-
2#           _      _    _ _
3#     _ __ (_)_ _ (_)__| | |__
4#    | '  \| | ' \| / _` | '_ \
5#    |_|_|_|_|_||_|_\__,_|_.__/
6#    simple python object store
7#
8# Copyright 2009-2010, 2014-2021 Thomas Perl <thp.io>. All rights reserved.
9#
10# Permission to use, copy, modify, and/or distribute this software for any
11# purpose with or without fee is hereby granted, provided that the above
12# copyright notice and this permission notice appear in all copies.
13#
14# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
15# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
16# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
17# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
18# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
19# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
20# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21#
22
23"""A simple SQLite3-based store for Python objects"""
24
25import sqlite3
26import threading
27import inspect
28import functools
29import types
30import collections
31import weakref
32import sys
33import json
34import datetime
35import logging
36
37
38__author__ = 'Thomas Perl <m@thp.io>'
39__version__ = '2.0.5'
40__url__ = 'http://thp.io/2010/minidb/'
41__license__ = 'ISC'
42
43
44__all__ = [
45    # Main classes
46    'Store', 'Model', 'JSON',
47
48    # Exceptions
49    'UnknownClass',
50
51    # Utility functions
52    'columns', 'func', 'literal',
53
54    # Decorator for registering converters
55    'converter_for',
56
57    # Debugging utilities
58    'pprint', 'pformat',
59]
60
61
62DEBUG_OBJECT_CACHE = False
63CONVERTERS = {}
64
65
66logger = logging.getLogger(__name__)
67
68
69class UnknownClass(TypeError):
70    ...
71
72
73def converter_for(type_):
74    def decorator(f):
75        CONVERTERS[type_] = f
76        return f
77
78    return decorator
79
80
81def _get_all_slots(class_, include_private=False):
82    for clazz in reversed(inspect.getmro(class_)):
83        if hasattr(clazz, '__minidb_slots__'):
84            for name, type_ in clazz.__minidb_slots__.items():
85                if include_private or not name.startswith('_'):
86                    yield (name, type_)
87
88
89def _set_attribute(o, slot, cls, value):
90    if value is None and hasattr(o.__class__, '__minidb_defaults__'):
91        value = getattr(o.__class__.__minidb_defaults__, slot, None)
92        if isinstance(value, types.FunctionType):
93            # Late-binding of default lambda (taking o as argument)
94            value = value(o)
95    if value is not None and cls not in CONVERTERS:
96        value = cls(value)
97    setattr(o, slot, value)
98
99
100class RowProxy(object):
101    def __init__(self, row, keys):
102        self._row = row
103        self._keys = keys
104
105    def __getitem__(self, key):
106        if isinstance(key, str):
107            try:
108                index = self._keys.index(key)
109            except ValueError:
110                raise KeyError(key)
111
112            return self._row[index]
113
114        return self._row[key]
115
116    def __getattr__(self, attr):
117        if attr not in self._keys:
118            raise AttributeError(attr)
119
120        return self[attr]
121
122    def __repr__(self):
123        return repr(self._row)
124
125    def keys(self):
126        return self._keys
127
128
129class Store(object):
130    PRIMARY_KEY = ('id', int)
131    MINIDB_ATTR = '_minidb'
132
133    def __init__(self, filename=':memory:', debug=False, smartupdate=False):
134        self.db = sqlite3.connect(filename, check_same_thread=False)
135        self.debug = debug
136        self.smartupdate = smartupdate
137        self.registered = {}
138        self.lock = threading.RLock()
139
140    def __enter__(self):
141        return self
142
143    def __exit__(self, exc_type, exc_value, traceback):
144        if exc_type is exc_value is traceback is None:
145            self.commit()
146
147        self.close()
148
149    def _execute(self, sql, args=None):
150        if args is None:
151            if self.debug:
152                logger.debug('%s', sql)
153            return self.db.execute(sql)
154        else:
155            if self.debug:
156                logger.debug('%s %r', sql, args)
157            return self.db.execute(sql, args)
158
159    def _schema(self, class_):
160        if class_ not in self.registered.values():
161            raise UnknownClass('{} was never registered'.format(class_))
162        return (class_.__name__, list(_get_all_slots(class_)))
163
164    def commit(self):
165        with self.lock:
166            self.db.commit()
167
168    def close(self):
169        with self.lock:
170            self.db.isolation_level = None
171            self._execute('VACUUM')
172            self.db.close()
173
174    def _ensure_schema(self, table, slots):
175        with self.lock:
176            cur = self._execute('PRAGMA table_info(%s)' % table)
177            available = cur.fetchall()
178
179            def column(name, type_, primary=True):
180                if (name, type_) == self.PRIMARY_KEY and primary:
181                    return 'INTEGER PRIMARY KEY'
182                elif type_ in (int, bool):
183                    return 'INTEGER'
184                elif type_ in (float,):
185                    return 'REAL'
186                elif type_ in (bytes,):
187                    return 'BLOB'
188                else:
189                    return 'TEXT'
190
191            if available:
192                available = [(row[1], row[2]) for row in available]
193
194                modify_slots = [(name, type_) for name, type_ in slots if name in (name for name, _ in available) and
195                                (name, column(name, type_, False)) not in available]
196                for name, type_ in modify_slots:
197                    raise TypeError('Column {} is {}, but expected {}'.format(name, next(dbtype for n, dbtype in
198                                                                                         available if n == name),
199                                                                              column(name, type_)))
200
201                # TODO: What to do with extraneous columns?
202
203                missing_slots = [(name, type_) for name, type_ in slots if name not in (n for n, _ in available)]
204                for name, type_ in missing_slots:
205                    self._execute('ALTER TABLE %s ADD COLUMN %s %s' % (table, name, column(name, type_)))
206            else:
207                self._execute('CREATE TABLE %s (%s)' % (table, ', '.join('{} {}'.format(name, column(name, type_))
208                                                                         for name, type_ in slots)))
209
210    def register(self, class_, upgrade=False):
211        if not issubclass(class_, Model):
212            raise TypeError('{} is not a subclass of minidb.Model'.format(class_.__name__))
213
214        if class_ in self.registered.values():
215            raise TypeError('{} is already registered'.format(class_.__name__))
216        elif class_.__name__ in self.registered and not upgrade:
217            raise TypeError('{} is already registered {}'.format(class_.__name__, self.registered[class_.__name__]))
218
219        with self.lock:
220            self.registered[class_.__name__] = class_
221            table, slots = self._schema(class_)
222            self._ensure_schema(table, slots)
223
224        return class_
225
226    def serialize(self, v, t):
227        if v is None:
228            return None
229        elif t in CONVERTERS:
230            return CONVERTERS[t](v, True)
231        elif isinstance(v, bool):
232            return int(v)
233        elif isinstance(v, (int, float, bytes)):
234            return v
235
236        return str(v)
237
238    def deserialize(self, v, t):
239        if v is None:
240            return None
241        elif t in CONVERTERS:
242            return CONVERTERS[t](v, False)
243        elif isinstance(v, t):
244            return v
245
246        return t(v)
247
248    def save_or_update(self, o):
249        if o.id is None:
250            o.id = self.save(o)
251        else:
252            self._update(o)
253
254    def delete_by_pk(self, o):
255        with self.lock:
256            table, slots = self._schema(o.__class__)
257
258            assert self.PRIMARY_KEY in slots
259            pk_name, pk_type = self.PRIMARY_KEY
260            pk = getattr(o, pk_name)
261            assert pk is not None
262
263            self._execute('DELETE FROM %s WHERE %s = ?' % (table, pk_name), [pk])
264            setattr(o, pk_name, None)
265
266    def _update(self, o):
267        with self.lock:
268            table, slots = self._schema(o.__class__)
269
270            # Update requires a primary key
271            assert self.PRIMARY_KEY in slots
272            pk_name, pk_type = self.PRIMARY_KEY
273
274            if self.smartupdate:
275                existing = dict(next(self.query(o.__class__, where=lambda c:
276                                                getattr(c, pk_name) == getattr(o, pk_name))))
277            else:
278                existing = {}
279
280            values = [(name, type_, getattr(o, name, None))
281                      for name, type_ in slots if (name, type_) != self.PRIMARY_KEY and
282                      (name not in existing or getattr(o, name, None) != existing[name])]
283
284            if self.smartupdate and self.debug:
285                for name, type_, to_value in values:
286                    logger.debug('%s %s', '{}(id={})'.format(table, o.id),
287                                 '{}: {} -> {}'.format(name, existing[name], to_value))
288
289            if not values:
290                # No values have changed - nothing to update
291                return
292
293            def gen_keys():
294                for name, type_, value in values:
295                    if value is not None:
296                        yield '{name}=?'.format(name=name)
297                    else:
298                        yield '{name}=NULL'.format(name=name)
299
300            def gen_values():
301                for name, type_, value in values:
302                    if value is not None:
303                        yield self.serialize(value, type_)
304
305                yield getattr(o, pk_name)
306
307            self._execute('UPDATE %s SET %s WHERE %s = ?' % (table, ', '.join(gen_keys()), pk_name), list(gen_values()))
308
309    def save(self, o):
310        with self.lock:
311            table, slots = self._schema(o.__class__)
312
313            # Save all values except for the primary key
314            slots = [(name, type_) for name, type_ in slots if (name, type_) != self.PRIMARY_KEY]
315
316            values = [self.serialize(getattr(o, name), type_) for name, type_ in slots]
317            return self._execute('INSERT INTO %s (%s) VALUES (%s)' % (table, ', '.join(name for name, type_ in slots),
318                                                                      ', '.join('?' * len(slots))), values).lastrowid
319
320    def delete_where(self, class_, where):
321        with self.lock:
322            table, slots = self._schema(class_)
323
324            if isinstance(where, types.FunctionType):
325                # Late-binding of where
326                where = where(class_.c)
327
328            ssql, args = where.tosql()
329            sql = 'DELETE FROM %s WHERE %s' % (table, ssql)
330            return self._execute(sql, args).rowcount
331
332    def query(self, class_, select=None, where=None, order_by=None, group_by=None, limit=None):
333        with self.lock:
334            table, slots = self._schema(class_)
335            attr_to_type = dict(slots)
336
337            sql = []
338            args = []
339
340            if select is None:
341                select = literal('*')
342
343            if isinstance(select, types.FunctionType):
344                # Late-binding of columns
345                select = select(class_.c)
346
347            # Select can always be a sequence
348            if not isinstance(select, Sequence):
349                select = Sequence([select])
350
351            # Look for RenameOperation operations in the SELECT sequence and
352            # remember the column types, so we can decode values properly later
353            for arg in select.args:
354                if isinstance(arg, Operation):
355                    if isinstance(arg.a, RenameOperation):
356                        if isinstance(arg.a.column, Column):
357                            attr_to_type[arg.a.name] = arg.a.column.type_
358
359            ssql, sargs = select.tosql()
360            sql.append('SELECT %s FROM %s' % (ssql, table))
361            args.extend(sargs)
362
363            if where is not None:
364                if isinstance(where, types.FunctionType):
365                    # Late-binding of columns
366                    where = where(class_.c)
367                wsql, wargs = where.tosql()
368                sql.append('WHERE %s' % (wsql,))
369                args.extend(wargs)
370
371            if order_by is not None:
372                if isinstance(order_by, types.FunctionType):
373                    # Late-binding of columns
374                    order_by = order_by(class_.c)
375
376                osql, oargs = order_by.tosql()
377                sql.append('ORDER BY %s' % (osql,))
378                args.extend(oargs)
379
380            if group_by is not None:
381                if isinstance(group_by, types.FunctionType):
382                    # Late-binding of columns
383                    group_by = group_by(class_.c)
384
385                gsql, gargs = group_by.tosql()
386                sql.append('GROUP BY %s' % (gsql,))
387                args.extend(gargs)
388
389            if limit is not None:
390                sql.append('LIMIT ?')
391                args.append(limit)
392
393            sql = ' '.join(sql)
394
395            result = self._execute(sql, args)
396            columns = [d[0] for d in result.description]
397
398            def _decode(row, columns):
399                for name, value in zip(columns, row):
400                    type_ = attr_to_type.get(name, None)
401                    yield (self.deserialize(value, type_) if type_ is not None else value)
402
403            return (RowProxy(tuple(_decode(row, columns)), columns) for row in result)
404
405    def load(self, class_, *args, **kwargs):
406        with self.lock:
407            query = kwargs.get('__query__', None)
408            if '__query__' in kwargs:
409                del kwargs['__query__']
410
411            table, slots = self._schema(class_)
412            sql = 'SELECT %s FROM %s' % (', '.join(name for name, type_ in slots), table)
413            if query:
414                if isinstance(query, types.FunctionType):
415                    # Late-binding of query
416                    query = query(class_.c)
417
418                ssql, aargs = query.tosql()
419                sql += ' WHERE %s' % ssql
420                sql_args = aargs
421            elif kwargs:
422                sql += ' WHERE %s' % (' AND '.join('%s = ?' % k for k in kwargs))
423                sql_args = list(kwargs.values())
424            else:
425                sql_args = []
426            cur = self._execute(sql, sql_args)
427
428            def apply(row):
429                row = zip(slots, row)
430                kwargs = {name: self.deserialize(v, type_) for (name, type_), v in row if v is not None}
431                o = class_(*args, **kwargs)
432                setattr(o, self.MINIDB_ATTR, self)
433                return o
434
435            return (x for x in (apply(row) for row in cur) if x is not None)
436
437    def get(self, class_, *args, **kwargs):
438        it = self.load(class_, *args, **kwargs)
439        result = next(it, None)
440
441        try:
442            next(it)
443        except StopIteration:
444            return result
445
446        raise ValueError('More than one row returned')
447
448
449class Operation(object):
450    def __init__(self, a, op=None, b=None, brackets=False):
451        self.a = a
452        self.op = op
453        self.b = b
454        self.brackets = brackets
455
456    def _get_class(self, a):
457        if isinstance(a, Column):
458            return a.class_
459        elif isinstance(a, RenameOperation):
460            return self._get_class(a.column)
461        elif isinstance(a, Function):
462            return a.args[0].class_
463        elif isinstance(a, Sequence):
464            return a.args[0].class_
465
466        raise ValueError('Cannot determine class for query')
467
468    def query(self, db, where=None, order_by=None, group_by=None, limit=None):
469        return self._get_class(self.a).query(db, self, where=where, order_by=order_by, group_by=group_by, limit=limit)
470
471    def __floordiv__(self, other):
472        if self.b is not None:
473            raise ValueError('Cannot sequence columns')
474        return Sequence([self, other])
475
476    def argtosql(self, arg):
477        if isinstance(arg, Operation):
478            return arg.tosql(self.brackets)
479        elif isinstance(arg, Column):
480            return (arg.name, [])
481        elif isinstance(arg, RenameOperation):
482            columnname, args = arg.column.tosql()
483            return ('%s AS %s' % (columnname, arg.name), args)
484        elif isinstance(arg, Function):
485            sqls = []
486            argss = []
487            for farg in arg.args:
488                sql, args = self.argtosql(farg)
489                sqls.append(sql)
490                argss.extend(args)
491            return ['%s(%s)' % (arg.name, ', '.join(sqls)), argss]
492        elif isinstance(arg, Sequence):
493            sqls = []
494            argss = []
495            for farg in arg.args:
496                sql, args = self.argtosql(farg)
497                sqls.append(sql)
498                argss.extend(args)
499            return ['%s' % ', '.join(sqls), argss]
500        elif isinstance(arg, Literal):
501            return [arg.name, []]
502        if type(arg) in CONVERTERS:
503            return ('?', [CONVERTERS[type(arg)](arg, True)])
504
505        return ('?', [arg])
506
507    def tosql(self, brackets=False):
508        sql = []
509        args = []
510
511        ssql, aargs = self.argtosql(self.a)
512        sql.append(ssql)
513        args.extend(aargs)
514
515        if self.op is not None:
516            sql.append(self.op)
517
518        if self.b is not None:
519            ssql, aargs = self.argtosql(self.b)
520            sql.append(ssql)
521            args.extend(aargs)
522
523        if brackets:
524            sql.insert(0, '(')
525            sql.append(')')
526
527        return (' '.join(sql), args)
528
529    def __and__(self, other):
530        return Operation(self, 'AND', other, True)
531
532    def __or__(self, other):
533        return Operation(self, 'OR', other, True)
534
535    def __repr__(self):
536        if self.b is None:
537            if self.op is None:
538                return '{self.a!r}'.format(self=self)
539            return '{self.a!r} {self.op}'.format(self=self)
540        return '{self.a!r} {self.op} {self.b!r}'.format(self=self)
541
542
543class Sequence(object):
544    def __init__(self, args):
545        self.args = args
546
547    def __repr__(self):
548        return ', '.join(repr(arg) for arg in self.args)
549
550    def tosql(self):
551        return Operation(self).tosql()
552
553    def query(self, db, order_by=None, group_by=None, limit=None):
554        return Operation(self).query(db, order_by=order_by, group_by=group_by, limit=limit)
555
556    def __floordiv__(self, other):
557        self.args.append(other)
558        return self
559
560
561def columns(*args):
562    """columns(a, b, c) -> a // b // c
563
564    Query multiple columns, like the // column sequence operator.
565    """
566    return Sequence(args)
567
568
569class func(object):
570    max = staticmethod(lambda *args: Function('max', *args))
571    min = staticmethod(lambda *args: Function('min', *args))
572    sum = staticmethod(lambda *args: Function('sum', *args))
573    distinct = staticmethod(lambda *args: Function('distinct', *args))
574    random = staticmethod(lambda: Function('random'))
575
576    abs = staticmethod(lambda a: Function('abs', a))
577    length = staticmethod(lambda a: Function('length', a))
578    lower = staticmethod(lambda a: Function('lower', a))
579    upper = staticmethod(lambda a: Function('upper', a))
580    ltrim = staticmethod(lambda a: Function('ltrim', a))
581    rtrim = staticmethod(lambda a: Function('rtrim', a))
582    trim = staticmethod(lambda a: Function('trim', a))
583
584    count = staticmethod(lambda a: Function('count', a))
585    __call__ = lambda a, name: RenameOperation(a, name)
586
587
588class OperatorMixin(object):
589    __lt__ = lambda a, b: Operation(a, '<', b)
590    __le__ = lambda a, b: Operation(a, '<=', b)
591    __eq__ = lambda a, b: Operation(a, '=', b) if b is not None else Operation(a, 'IS NULL')
592    __ne__ = lambda a, b: Operation(a, '!=', b) if b is not None else Operation(a, 'IS NOT NULL')
593    __gt__ = lambda a, b: Operation(a, '>', b)
594    __ge__ = lambda a, b: Operation(a, '>=', b)
595
596    __call__ = lambda a, name: RenameOperation(a, name)
597    tosql = lambda a: Operation(a).tosql()
598    query = lambda a, db, where=None, order_by=None, group_by=None, limit=None: Operation(a).query(db, where=where,
599                                                                                                   order_by=order_by,
600                                                                                                   group_by=group_by,
601                                                                                                   limit=limit)
602    __floordiv__ = lambda a, b: Sequence([a, b])
603
604    like = lambda a, b: Operation(a, 'LIKE', b)
605
606    avg = property(lambda a: Function('avg', a))
607    max = property(lambda a: Function('max', a))
608    min = property(lambda a: Function('min', a))
609    sum = property(lambda a: Function('sum', a))
610    distinct = property(lambda a: Function('distinct', a))
611
612    asc = property(lambda a: Operation(a, 'ASC'))
613    desc = property(lambda a: Operation(a, 'DESC'))
614
615    abs = property(lambda a: Function('abs', a))
616    length = property(lambda a: Function('length', a))
617    lower = property(lambda a: Function('lower', a))
618    upper = property(lambda a: Function('upper', a))
619    ltrim = property(lambda a: Function('ltrim', a))
620    rtrim = property(lambda a: Function('rtrim', a))
621    trim = property(lambda a: Function('trim', a))
622    count = property(lambda a: Function('count', a))
623
624
625class RenameOperation(OperatorMixin):
626    def __init__(self, column, name):
627        self.column = column
628        self.name = name
629
630    def __repr__(self):
631        return '%r AS %s' % (self.column, self.name)
632
633
634class Literal(OperatorMixin):
635    def __init__(self, name):
636        self.name = name
637
638    def __repr__(self):
639        return self.name
640
641
642def literal(name):
643    """Insert a literal as-is into a SQL query
644
645    >>> func.count(literal('*'))
646    count(*)
647    """
648    return Literal(name)
649
650
651class Function(OperatorMixin):
652    def __init__(self, name, *args):
653        self.name = name
654        self.args = args
655
656    def __repr__(self):
657        return '%s(%s)' % (self.name, ', '.join(repr(arg) for arg in self.args))
658
659
660class Column(OperatorMixin):
661    def __init__(self, class_, name, type_):
662        self.class_ = class_
663        self.name = name
664        self.type_ = type_
665
666    def __repr__(self):
667        return '.'.join((self.class_.__name__, self.name))
668
669
670class Columns(object):
671    def __init__(self, name, slots):
672        self._class = None
673        self._name = name
674        self._slots = slots
675
676    def __repr__(self):
677        return '<{} for {} ({})>'.format(self.__class__.__name__, self._name, ', '.join(self._slots))
678
679    def __getattr__(self, name):
680        d = {k: v for k, v in _get_all_slots(self._class, include_private=True)}
681        if name not in d:
682            raise AttributeError(name)
683
684        return Column(self._class, name, d[name])
685
686
687def model_init(self, *args, **kwargs):
688    slots = list(_get_all_slots(self.__class__, include_private=True))
689    unmatched_kwargs = set(kwargs.keys()).difference(set(key for key, type_ in slots))
690    if unmatched_kwargs:
691        raise KeyError('Invalid keyword argument(s): %r' % unmatched_kwargs)
692
693    for key, type_ in slots:
694        _set_attribute(self, key, type_, kwargs.get(key, None))
695
696    # Call redirected constructor
697    if '__minidb_init__' in self.__class__.__dict__:
698        getattr(self, '__minidb_init__')(*args)
699
700
701class MetaModel(type):
702    @classmethod
703    def __prepare__(metacls, name, bases):
704        return collections.OrderedDict()
705
706    def __new__(mcs, name, bases, d):
707        # Redirect __init__() to __minidb_init__()
708        if '__init__' in d:
709            d['__minidb_init__'] = d['__init__']
710        d['__init__'] = model_init
711
712        # Caching of live objects
713        d['__minidb_cache__'] = weakref.WeakValueDictionary()
714
715        slots = collections.OrderedDict((k, v) for k, v in d.items()
716                                        if k.lower() == k and
717                                        not k.startswith('__') and
718                                        not isinstance(v, types.FunctionType) and
719                                        not isinstance(v, property) and
720                                        not isinstance(v, staticmethod) and
721                                        not isinstance(v, classmethod))
722
723        keep = collections.OrderedDict((k, v) for k, v in d.items() if k not in slots)
724        keep['__minidb_slots__'] = slots
725
726        keep['__slots__'] = tuple(slots.keys())
727        if not bases:
728            # Add weakref slot to Model (for caching)
729            keep['__slots__'] += ('__weakref__',)
730
731        columns = Columns(name, slots)
732        keep['c'] = columns
733
734        result = type.__new__(mcs, name, bases, keep)
735        columns._class = result
736        return result
737
738
739def pformat(result, color=False):
740    def incolor(color_id, s):
741        return '\033[9%dm%s\033[0m' % (color_id, s) if sys.stdout.isatty() and color else s
742
743    inred, ingreen, inyellow, inblue = (functools.partial(incolor, x) for x in range(1, 5))
744
745    rows = list(result)
746    if not rows:
747        return '(no rows)'
748
749    def colorvalue(formatted, value):
750        if value is None:
751            return inred(formatted)
752        if isinstance(value, bool):
753            return ingreen(formatted)
754
755        return formatted
756
757    s = []
758    keys = rows[0].keys()
759    lengths = tuple(max(x) for x in zip(*[[len(str(column)) for column in row] for row in [keys] + rows]))
760    s.append(' | '.join(inyellow('%-{}s'.format(length) % key) for key, length in zip(keys, lengths)))
761    s.append('-+-'.join('-' * length for length in lengths))
762    for row in rows:
763        s.append(' | '.join(colorvalue('%-{}s'.format(length) % col, col) for col, length in zip(row, lengths)))
764    s.append('({} row(s))'.format(len(rows)))
765    return ('\n'.join(s))
766
767
768def pprint(result, color=False):
769    print(pformat(result, color))
770
771
772class JSON(object):
773    ...
774
775
776@converter_for(JSON)
777def convert_json(v, serialize):
778    return json.dumps(v) if serialize else json.loads(v)
779
780
781@converter_for(datetime.datetime)
782def convert_datetime_datetime(v, serialize):
783    """
784    >>> convert_datetime_datetime(datetime.datetime(2014, 12, 13, 14, 15), True)
785    '2014-12-13T14:15:00'
786    >>> convert_datetime_datetime('2014-12-13T14:15:16', False)
787    datetime.datetime(2014, 12, 13, 14, 15, 16)
788    """
789    if serialize:
790        return v.isoformat()
791    else:
792        isoformat, microseconds = (v.rsplit('.', 1) if '.' in v else (v, 0))
793        return (datetime.datetime.strptime(isoformat, '%Y-%m-%dT%H:%M:%S') +
794                datetime.timedelta(microseconds=int(microseconds)))
795
796
797@converter_for(datetime.date)
798def convert_datetime_date(v, serialize):
799    """
800    >>> convert_datetime_date(datetime.date(2014, 12, 13), True)
801    '2014-12-13'
802    >>> convert_datetime_date('2014-12-13', False)
803    datetime.date(2014, 12, 13)
804    """
805    if serialize:
806        return v.isoformat()
807    else:
808        return datetime.datetime.strptime(v, '%Y-%m-%d').date()
809
810
811@converter_for(datetime.time)
812def convert_datetime_time(v, serialize):
813    """
814    >>> convert_datetime_time(datetime.time(14, 15, 16), True)
815    '14:15:16'
816    >>> convert_datetime_time('14:15:16', False)
817    datetime.time(14, 15, 16)
818    """
819    if serialize:
820        return v.isoformat()
821    else:
822        isoformat, microseconds = (v.rsplit('.', 1) if '.' in v else (v, 0))
823        return (datetime.datetime.strptime(isoformat, '%H:%M:%S') +
824                datetime.timedelta(microseconds=int(microseconds))).time()
825
826
827class Model(metaclass=MetaModel):
828    id = int
829    _minidb = Store
830
831    @classmethod
832    def _finalize(cls, id):
833        if DEBUG_OBJECT_CACHE:
834            logger.debug('Finalizing {} id={}'.format(cls.__name__, id))
835
836    def __repr__(self):
837        def get_attrs():
838            for key, type_ in _get_all_slots(self.__class__):
839                yield key, getattr(self, key, None)
840
841        attrs = ['{key}={value!r}'.format(key=key, value=value) for key, value in get_attrs()]
842        return '<%(cls)s(%(attrs)s)>' % {
843            'cls': self.__class__.__name__,
844            'attrs': ', '.join(attrs),
845        }
846
847    @classmethod
848    def __lookup_single(cls, o):
849        if o is None:
850            return None
851
852        cache = cls.__minidb_cache__
853        if o.id not in cache:
854            if DEBUG_OBJECT_CACHE:
855                logger.debug('Storing id={} in cache {}'.format(o.id, o))
856                weakref.finalize(o, cls._finalize, o.id)
857            cache[o.id] = o
858        else:
859            if DEBUG_OBJECT_CACHE:
860                logger.debug('Getting id={} from cache'.format(o.id))
861        return cache[o.id]
862
863    @classmethod
864    def __lookup_cache(cls, objects):
865        for o in objects:
866            yield cls.__lookup_single(o)
867
868    @classmethod
869    def load(cls, db, query=None, **kwargs):
870        if query is not None:
871            kwargs['__query__'] = query
872        if '__minidb_init__' in cls.__dict__:
873            @functools.wraps(cls.__minidb_init__)
874            def init_wrapper(*args):
875                return cls.__lookup_cache(db.load(cls, *args, **kwargs))
876            return init_wrapper
877        else:
878            return cls.__lookup_cache(db.load(cls, **kwargs))
879
880    @classmethod
881    def get(cls, db, query=None, **kwargs):
882        if query is not None:
883            kwargs['__query__'] = query
884        if '__minidb_init__' in cls.__dict__:
885            @functools.wraps(cls.__minidb_init__)
886            def init_wrapper(*args):
887                return cls.__lookup_single(db.get(cls, *args, **kwargs))
888            return init_wrapper
889        else:
890            return cls.__lookup_single(db.get(cls, **kwargs))
891
892    def save(self, db=None):
893        if getattr(self, Store.MINIDB_ATTR, None) is None:
894            if db is None:
895                raise ValueError('Needs a db object')
896            setattr(self, Store.MINIDB_ATTR, db)
897
898        getattr(self, Store.MINIDB_ATTR).save_or_update(self)
899
900        if DEBUG_OBJECT_CACHE:
901            logger.debug('Storing id={} in cache {}'.format(self.id, self))
902            weakref.finalize(self, self.__class__._finalize, self.id)
903        self.__class__.__minidb_cache__[self.id] = self
904
905        return self
906
907    def delete(self):
908        if getattr(self, Store.MINIDB_ATTR) is None:
909            raise ValueError('Needs a db object')
910        elif self.id is None:
911            raise KeyError('id is None (not stored in db?)')
912
913        # drop from cache
914        cache = self.__class__.__minidb_cache__
915        if self.id in cache:
916            if DEBUG_OBJECT_CACHE:
917                logger.debug('Dropping id={} from cache {}'.format(self.id, self))
918            del cache[self.id]
919
920        getattr(self, Store.MINIDB_ATTR).delete_by_pk(self)
921
922    @classmethod
923    def delete_where(cls, db, query):
924        return db.delete_where(cls, query)
925
926    @classmethod
927    def query(cls, db, select=None, where=None, order_by=None, group_by=None, limit=None):
928        return db.query(cls, select=select, where=where, order_by=order_by, group_by=group_by, limit=limit)
929
930    @classmethod
931    def pquery(cls, db, select=None, where=None, order_by=None, group_by=None, limit=None, color=True):
932        pprint(db.query(cls, select=select, where=where, order_by=order_by, group_by=group_by, limit=limit), color)
933