from peewee import * from playhouse.hybrid import * from playhouse.sqlite_ext import SqliteExtDatabase, ClosureTable import pathlib import datetime from enum import Enum, unique from pkg_resources import resource_filename from .peewee_ext import EnumField, CharField, CompressedField, SQLIndex from .peewee_fixes import * # dummy to force evaluation of those fixes db = SqliteExtDatabase('test.db', pragmas=[('foreign_keys', 'ON')]) db.load_extension(resource_filename(__name__, 'sqlext/closure')) db.set_autocommit(False) __tables__ = [] __all__ = ['create_tables', 'drop_tables'] def table(cls): __tables__.append(cls) __all__.append(cls.__name__) return cls def create_tables(): db.create_tables(__tables__, True) def drop_tables(): db.drop_tables(__tables__, True) class BaseModel(Model): __keys__ = [] @classmethod def try_create(cls, **kwargs): try: return cls.create(**kwargs) except IntegrityError: return None @classmethod def create_or_get(cls, **kwargs): if not cls.__keys__: raise ValueError("create_or_get not supported on %s" % cls.__name__) for key in cls.__keys__: if key not in kwargs: raise ValueError("required key '%s' missing" % key) try: return cls.create(**kwargs), True except IntegrityError: sel = {k:kwargs[k] for k in cls.__keys__} return cls.get(**sel), False class Meta: database = db @table class DocumentContent(BaseModel): blob = CompressedField() @table class Document(BaseModel): @unique class Direction(Enum): IN = 0 OUT = 1 content = ForeignKeyField(DocumentContent, related_name = 'document') created = DateField(default=datetime.date.today) inserted = DateTimeField(default=datetime.datetime.now) name = CharField(null=True) original_path = CharField(null=True) mimetype = CharField(null=True) direction = EnumField(Direction, null=True) @classmethod @db.atomic() def create_from_file(cls, from_file, tags, **kwargs): content = DocumentContent.create(blob=from_file.read()) doc = cls.create(content = content, original_path = pathlib.PurePath(from_file.name).as_posix(), **kwargs) for t in tags: DocumentTag.create(document = doc, tag = t) return doc @property def path(self): return pathlib.PurePosixPath(self.original_path) @table class Prefix(BaseModel): __keys__ = ('name',) name = CharField(primary_key = True, nocase=True) virtual = BooleanField(default = False) description = CharField(null=True) @classmethod @db.atomic() def create(cls, **query): inst = super().create(**query) Tag.create_prefix_default(inst) return inst @property def default_tag(self): return Tag.get(prefix = self, default = True) def __str__ (self): return self.name def __repr__ (self): ext = ' (virt)' if self.virtual else '' return "<%s %s%s>" % (self.__class__.__name__, self.name, ext) @table class Tag(BaseModel): __keys__ = ('name', 'prefix') name = CharField(nocase=True) prefix = ForeignKeyField(Prefix, null=True, related_name = 'tag', db_column = 'prefix') description = CharField(null=True) default = BooleanField(default = False) @classmethod def create_prefix_default(cls,prefix): return cls.create(name = '*', prefix = prefix, description = "Default for prefix '%s'" % prefix, default = True) @hybrid_method def matches(self, prefix, name): if isinstance(prefix, Prefix): prefix = prefix.name return (self.prefix_id == prefix) & (self.name == name) class Meta: indexes = [ (('name', 'prefix'), False), (('name', SQLIndex('coalescePrefix', "COALESCE(prefix,'')")), True) ] @property def prefixed_name(self): if self.prefix_id: return "%s:%s" % (self.prefix_id, self.name) else: return self.name def __str__(self): return self.prefixed_name def __repr__(self): return "<%s (#%d) %s>" % (self.__class__.__name__, self.id, self.prefixed_name) @table class DocumentTag(BaseModel): document = ForeignKeyField(Document, related_name = 'tags') tag = ForeignKeyField(Tag) class Meta: primary_key = CompositeKey('document', 'tag') def __repr__(self): return "<%s %d --> %d>" % (self.__class__.__name__, self.document_id, self.tag_id) @table class TagImplications(BaseModel): tag = ForeignKeyField(Tag, related_name = 'implications') implies_tag = ForeignKeyField(Tag, related_name = '_implied_by') class Meta: primary_key = CompositeKey('tag', 'implies_tag') def __repr__(self): return "<%s %d --> %d>" % (self.__class__.__name__, self.tag_id, self.implies_tag_id) TagClosure = table(ClosureTable( Tag, referencing_class = TagImplications, foreign_key = TagImplications.implies_tag, referencing_key = TagImplications.tag))