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