class File(db.Model, IntBase): __tablename__ = 'grano_file' file_name = db.Column(db.Unicode) mime_type = db.Column(db.Unicode) project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) data = db.Column(db.LargeBinary) properties = db.relationship('Property', backref='value_file', cascade='all, delete, delete-orphan', lazy='dynamic') @property def fh(self): return StringIO(self.data) def to_dict_index(self): return { 'id': self.id, 'project': self.project.to_dict_index(), 'api_url': url_for('files_api.view', id=self.id), 'serve_api_url': url_for('files_api.serve', id=self.id), 'file_name': self.file_name, 'mime_type': self.mime_type } def to_dict(self): """ Full serialization of the file metadata. """ return self.to_dict_index()
class Pipeline(db.Model, IntBase): __tablename__ = 'grano_pipeline' STATUS_PENDING = 'pending' STATUS_RUNNING = 'running' STATUS_FAILED = 'failed' STATUS_COMPLETE = 'complete' operation = db.Column(db.Unicode) label = db.Column(db.Unicode) status = db.Column(db.Unicode) percent_complete = db.Column(db.Integer, default=int) project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) config = db.Column(MutableDict.as_mutable(JSONEncodedDict)) started_at = db.Column(db.DateTime, default=datetime.utcnow) ended_at = db.Column(db.DateTime) entries = db.relationship('LogEntry', backref='pipeline', lazy='dynamic', cascade='all, delete, delete-orphan') def has_errors(self): q = self.entries.filter_by(level=logging.ERROR) return q.count() > 0 def to_dict_index(self): return { 'id': self.id, 'label': self.label, 'project': self.project.to_dict_short() if self.project else None, 'author': self.author.to_dict_index(), 'api_url': url_for('pipelines_api.view', id=self.id), 'operation': self.operation, 'status': self.status, 'created_at': self.created_at, 'updated_at': self.updated_at, 'started_at': self.started_at, 'ended_at': self.ended_at, 'config': self.config, 'percent_complete': self.percent_complete } def to_dict(self): """ Full serialization of the file metadata. """ data = self.to_dict_index() return data
class EntityProperty(Property): __mapper_args__ = {'polymorphic_identity': 'entity'} entity_id = db.Column(db.Unicode(), db.ForeignKey('entity.id'), index=True) def _set_obj(self, obj): self.entity = obj
class Schema(db.Model, IntBase): __tablename__ = 'schema' name = db.Column(db.Unicode()) label = db.Column(db.Unicode()) label_in = db.Column(db.Unicode()) label_out = db.Column(db.Unicode()) hidden = db.Column(db.Boolean()) obj = db.Column(db.Unicode()) attributes = db.relationship(Attribute, backref='schema', lazy='joined') properties = db.relationship(Property, backref='schema', lazy='dynamic') relations = db.relationship('Relation', backref='schema', lazy='dynamic') project_id = db.Column(db.Integer, db.ForeignKey('project.id')) def get_attribute(self, name): for attribute in self.attributes: if attribute.name == name: return attribute @classmethod def by_name(cls, project, name): q = db.session.query(cls).filter_by(name=name) q = q.filter_by(project=project) return q.first() @classmethod def by_obj_name(cls, project, obj, name): q = db.session.query(cls) q = q.filter_by(project=project) q = q.filter_by(name=name) q = q.filter_by(obj=obj) return q.first()
class Project(db.Model, IntBase): __tablename__ = 'grano_project' slug = db.Column(db.Unicode) label = db.Column(db.Unicode) private = db.Column(db.Boolean, default=False) settings = db.Column(MutableDict.as_mutable(JSONEncodedDict)) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) relations = db.relationship('Relation', backref='project', lazy='dynamic', cascade='all, delete, delete-orphan') entities = db.relationship('Entity', backref='project', lazy='dynamic', cascade='all, delete, delete-orphan') pipelines = db.relationship('Pipeline', backref='project', lazy='dynamic', cascade='all, delete, delete-orphan') schemata = db.relationship('Schema', backref='project', lazy='dynamic', cascade='all, delete, delete-orphan') permissions = db.relationship('Permission', backref='project', lazy='dynamic', cascade='all, delete, delete-orphan') files = db.relationship('File', backref='project', lazy='dynamic', cascade='all, delete, delete-orphan') def get_attribute(self, obj, name): for schema in self.schemata: if schema.obj == obj: for attr in schema.attributes: if attr.name == name: return attr @classmethod def by_slug(cls, slug): q = db.session.query(cls).filter_by(slug=slug) return q.first() def to_dict_index(self): return { 'slug': self.slug, 'label': self.label, 'private': self.private, 'api_url': url_for('projects_api.view', slug=self.slug), 'entities_count': self.entities.count(), 'relations_count': self.relations.count() } def to_dict(self): data = self.to_dict_index() data['settings'] = self.settings data['author'] = self.author.to_dict_index() data['schemata_index_url'] = url_for('schemata_api.index', slug=self.slug) data['entities_index_url'] = url_for('entities_api.index', project=self.slug) data['relations_index_url'] = url_for('relations_api.index', project=self.slug) return data
class Relation(db.Model, UUIDBase, PropertyBase): __tablename__ = 'relation' PropertyClass = RelationProperty schema_id = db.Column(db.Integer, db.ForeignKey('schema.id'), index=True) source_id = db.Column(db.Unicode, db.ForeignKey('entity.id'), index=True) target_id = db.Column(db.Unicode, db.ForeignKey('entity.id'), index=True) project_id = db.Column(db.Integer, db.ForeignKey('project.id')) author_id = db.Column(db.Integer, db.ForeignKey('account.id')) properties = db.relationship(RelationProperty, order_by=RelationProperty.created_at.desc(), backref='relation', lazy='dynamic') @property def schemata(self): return [self.schema]
class RelationProperty(Property): __mapper_args__ = {'polymorphic_identity': 'relation'} relation_id = db.Column(db.Unicode(), db.ForeignKey('relation.id'), index=True) def _set_obj(self, obj): self.relation = obj
class Relation(db.Model, UUIDBase, PropertyBase): __tablename__ = 'grano_relation' #PropertyClass = RelationProperty schema_id = db.Column(db.Integer, db.ForeignKey('grano_schema.id'), index=True) source_id = db.Column(db.Unicode, db.ForeignKey('grano_entity.id'), index=True) target_id = db.Column(db.Unicode, db.ForeignKey('grano_entity.id'), index=True) project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) properties = db.relationship(Property, order_by=Property.created_at.desc(), cascade='all, delete, delete-orphan', backref='relation', lazy='dynamic') @property def schemata(self): return [self.schema] def to_dict_base(self): return { 'id': self.id, 'properties': {}, 'project': self.project.to_dict_index(), 'api_url': url_for('relations_api.view', id=self.id), 'schema': self.schema.to_dict_index(), 'source': self.source.to_dict_index(), 'target': self.target.to_dict_index() } def to_dict(self): data = self.to_dict_base() for prop in self.active_properties: name, prop = prop.to_dict_kv() data['properties'][name] = prop return data def to_dict_index(self): data = self.to_dict_base() for prop in self.active_properties: name, prop = prop.to_dict_kv() data['properties'][name] = prop return data
class Attribute(db.Model, IntBase): __tablename__ = 'grano_attribute' DATATYPES = { 'string': 'value_string', 'integer': 'value_integer', 'float': 'value_float', 'datetime': 'value_datetime', 'boolean': 'value_boolean', 'file': 'value_file_id' } name = db.Column(db.Unicode()) label = db.Column(db.Unicode()) description = db.Column(db.Unicode()) hidden = db.Column(db.Boolean()) datatype = db.Column(db.Unicode()) schema_id = db.Column(db.Integer, db.ForeignKey('grano_schema.id')) properties = db.relationship('Property', backref='attribute', cascade='all, delete, delete-orphan', lazy='dynamic') @property def value_column(self): return self.DATATYPES.get(self.datatype) @classmethod def all_named(cls, name): q = db.session.query(cls) q = q.filter_by(name=name) return q.all() @classmethod def by_schema_and_name(cls, schema, name): q = db.session.query(cls) q = q.filter_by(schema=schema) q = q.filter_by(name=name) return q.first() def to_index(self): return { 'name': self.name, 'label': self.label, 'datatype': self.datatype } def to_dict(self): data = self.to_index() data['id'] = self.id data['hidden'] = self.hidden if self.description and len(self.description): data['description'] = self.description return data
class Attribute(db.Model): """ Attributes are specific properties of a schema for either an entity or a relation. They materialize as columns on the joined sub-table for the schema. """ __tablename__ = 'schema_attribute' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode) label = db.Column(db.Unicode) type = db.Column(db.Unicode) help = db.Column(db.Unicode) missing = db.Column(db.Unicode) schema_id = db.Column(db.Integer, db.ForeignKey('schema.id')) schema = db.relationship(Schema, backref=db.backref('attributes', lazy='dynamic')) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) @classmethod def create(cls, name, data): obj = cls() obj.update(name, data) return obj def update(self, name, data): self.name = name self.label = data.get('label') self.type = data.get('type') self.help = data.get('help') self.missing = data.get('missing') db.session.add(self) db.session.flush() def delete(self): db.session.delete(self) def as_dict(self): return { 'label': self.label, 'type': self.type, 'help': self.help, 'missing': self.missing } @property def column(self): # TODO: do we also need some typecasting mechanism? type_ = ATTRIBUTE_TYPES_DB[self.type] return db.Column(self.name, type_) def __repr__(self): return "<Attribute(%s,%s)>" % (self.id, self.name)
class Property(db.Model, IntBase): __tablename__ = 'property' schema_id = db.Column(db.Integer, db.ForeignKey('schema.id')) author_id = db.Column(db.Integer, db.ForeignKey('account.id')) name = db.Column(db.Unicode(), index=True) value = db.Column(db.Unicode()) source_url = db.Column(db.Unicode()) active = db.Column(db.Boolean()) obj = db.Column(db.String(20)) __mapper_args__ = {'polymorphic_on': obj} def to_dict(self): return { 'name': self.name, 'schema': self.schema.name, 'value': self.value, 'source_url': self.source_url, 'active': self.active }
class Property(db.Model, IntBase): __tablename__ = 'grano_property' schema_id = db.Column(db.Integer, db.ForeignKey('grano_schema.id')) attribute_id = db.Column(db.Integer, db.ForeignKey('grano_attribute.id')) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) entity_id = db.Column(db.Unicode(), db.ForeignKey('grano_entity.id'), index=True, nullable=True) relation_id = db.Column(db.Unicode(), db.ForeignKey('grano_relation.id'), index=True, nullable=True) name = db.Column(db.Unicode(), index=True) value_string = db.Column(db.Unicode()) value_integer = db.Column(db.Integer()) value_float = db.Column(db.Float()) value_datetime = db.Column(db.DateTime()) value_boolean = db.Column(db.Boolean()) value_file_id = db.Column(db.Integer(), db.ForeignKey('grano_file.id')) source_url = db.Column(db.Unicode()) active = db.Column(db.Boolean()) @property def value(self): # check file column first since file uses both # value_string and value_file_id if self.value_file_id is not None: return self.value_file_id for column in Attribute.DATATYPES.values(): value = getattr(self, column) if value is not None: return value def to_dict_index(self): data = {'value': self.value, 'source_url': self.source_url} if self.value_file_id is not None: data['file_url'] = self.value_string return data def to_dict_kv(self): return self.name, self.to_dict_index() def to_dict(self): name, data = self.to_dict_index() data['id'] = self.id data['name'] = name data['created_at'] = self.created_at data['updated_at'] = self.updated_at data['active'] = self.active return data
class Attribute(db.Model, IntBase): __tablename__ = 'attribute' name = db.Column(db.Unicode()) label = db.Column(db.Unicode()) description = db.Column(db.Unicode()) hidden = db.Column(db.Boolean()) schema_id = db.Column(db.Integer, db.ForeignKey('schema.id')) @classmethod def by_name(cls, schema, name): q = db.session.query(cls) q = q.filter_by(schema=schema) q = q.filter_by(name=name) return q.first()
class Permission(db.Model, IntBase): __tablename__ = 'grano_permission' reader = db.Column(db.Boolean) editor = db.Column(db.Boolean) admin = db.Column(db.Boolean) account_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) @classmethod def by_project_and_id(cls, project, id): q = db.session.query(cls).filter_by(id=id) q = q.filter_by(project=project) return q.first() def to_dict_index(self): return { 'id': self.id, 'reader': self.reader, 'editor': self.editor, 'admin': self.admin, 'project': self.project.to_dict_index(), 'account': self.account.to_dict_index(), 'api_url': url_for('permissions_api.view', slug=self.project.slug, id=self.id) } def to_dict(self): return self.to_dict_index()
class Project(db.Model, IntBase): __tablename__ = 'project' slug = db.Column(db.Unicode) label = db.Column(db.Unicode) settings = db.Column(MutableDict.as_mutable(JSONEncodedDict)) author_id = db.Column(db.Integer, db.ForeignKey('account.id')) relations = db.relationship('Relation', backref='project', lazy='dynamic') entities = db.relationship('Entity', backref='project', lazy='dynamic') schemata = db.relationship('Schema', backref='project', lazy='dynamic') @classmethod def by_slug(cls, slug): q = db.session.query(cls).filter_by(slug=slug) return q.first()
class LogEntry(db.Model, IntBase): __tablename__ = 'grano_log_entry' pipeline_id = db.Column(db.Integer, db.ForeignKey('grano_pipeline.id')) level = db.Column(db.Integer) message = db.Column(db.Unicode) error = db.Column(db.Unicode) details = db.Column(MutableDict.as_mutable(JSONEncodedDict)) def to_dict_index(self): return { 'id': self.id, 'level': self.level, 'message': self.message, 'error': self.error, 'api_url': url_for('log_entries_api.view_entry', pipeline_id=self.pipeline.id, id=self.id), 'created_at': self.created_at, 'updated_at': self.updated_at } def to_dict(self): """ Full serialization of the file metadata. """ data = self.to_dict_index() data['details'] = self.details return data
class Property(db.Model, IntBase): __tablename__ = 'grano_property' attribute_id = db.Column(db.Integer, db.ForeignKey('grano_attribute.id')) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) entity_id = db.Column(db.Unicode(), db.ForeignKey('grano_entity.id'), index=True, nullable=True) relation_id = db.Column(db.Unicode(), db.ForeignKey('grano_relation.id'), index=True, nullable=True) name = db.Column(db.Unicode(), index=True) value_string = db.Column(db.Unicode()) value_integer = db.Column(db.Integer()) value_float = db.Column(db.Float()) value_datetime = db.Column(db.DateTime()) value_datetime_precision = db.Column( db.Enum(*DATETIME_PRECISION, native_enum=False)) value_boolean = db.Column(db.Boolean()) value_file_id = db.Column(db.Integer(), db.ForeignKey('grano_file.id')) source_url = db.Column(db.Unicode()) active = db.Column(db.Boolean()) @property def value(self): # check file column first since file uses both # value_string and value_file_id if self.value_file_id is not None: return self.value_file_id for column in Attribute.DATATYPES.values(): value = getattr(self, column) if value is not None: return value @classmethod def type_column(self, value): for name, typ in VALUE_COLUMNS.items(): if isinstance(value, typ): return name return 'value_string' def to_dict_index(self): data = {'value': self.value, 'source_url': self.source_url} if self.value_file_id is not None: data['file_url'] = self.value_string elif self.value_datetime is not None: data['value_precision'] = self.value_datetime_precision return data def to_dict_kv(self): return self.name, self.to_dict_index() def to_dict(self): name, data = self.to_dict_index() data['id'] = self.id data['name'] = name data['created_at'] = self.created_at data['updated_at'] = self.updated_at data['active'] = self.active return data
class Entity(db.Model, UUIDBase, PropertyBase): __tablename__ = 'grano_entity' same_as = db.Column(db.Unicode, db.ForeignKey('grano_entity.id'), nullable=True) project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) schema_id = db.Column(db.Integer, db.ForeignKey('grano_schema.id'), index=True) degree_in = db.Column(db.Integer) degree_out = db.Column(db.Integer) degree = db.Column(db.Integer) inbound = db.relationship('Relation', lazy='dynamic', backref='target', primaryjoin='Entity.id==Relation.target_id', cascade='all, delete, delete-orphan') outbound = db.relationship('Relation', lazy='dynamic', backref='source', primaryjoin='Entity.id==Relation.source_id', cascade='all, delete, delete-orphan') properties = db.relationship(Property, backref='entity', order_by=Property.created_at.desc(), cascade='all, delete, delete-orphan', lazy='joined') @property def names(self): return [p for p in self.properties if p.name == 'name'] @classmethod def by_name(cls, project, name, only_active=False): q = cls.by_name_many(project, name, only_active=only_active) return q.first() @classmethod def by_name_many(cls, project, name, only_active=False): q = db.session.query(cls) q = q.filter(cls.project == project) q = cls._filter_property(q, 'name', name, only_active=only_active) return q def to_dict_index(self): """ Convert an entity to the REST API form. """ data = { 'id': self.id, 'degree': self.degree, 'degree_in': self.degree_in, 'degree_out': self.degree_out, 'project': self.project.to_dict_short(), 'schema': self.schema.to_dict_index(), 'api_url': url_for('entities_api.view', id=self.id), 'properties': {} } for prop in self.active_properties: name, prop = prop.to_dict_kv() data['properties'][name] = prop if self.same_as: data['same_as'] = self.same_as data['same_as_url'] = url_for('entities_api.view', id=self.same_as) return data def to_dict(self): """ Full serialization of the entity. """ data = self.to_dict_index() data['created_at'] = self.created_at data['updated_at'] = self.updated_at if data['degree_in'] > 0: data['inbound_url'] = url_for('relations_api.index', target=self.id) if data['degree_out'] > 0: data['outbound_url'] = url_for('relations_api.index', source=self.id) return data def to_index(self): """ Convert an entity to a form appropriate for search indexing. """ data = self.to_dict() data['names'] = [] for prop in self.properties: if prop.name == 'name': data['names'].append(prop.value) return data
from grano.core import db from grano.model.common import UUIDBase, PropertyBase from grano.model.schema import Schema from grano.model.relation import Relation from grano.model.property import EntityProperty entity_schema = db.Table( 'entity_schema', db.Column('entity_id', db.Unicode, db.ForeignKey('entity.id')), db.Column('schema_id', db.Integer, db.ForeignKey('schema.id'))) class Entity(db.Model, UUIDBase, PropertyBase): __tablename__ = 'entity' PropertyClass = EntityProperty same_as = db.Column(db.Unicode, db.ForeignKey('entity.id'), nullable=True) project_id = db.Column(db.Integer, db.ForeignKey('project.id')) author_id = db.Column(db.Integer, db.ForeignKey('account.id')) schemata = db.relationship('Schema', secondary=entity_schema, backref=db.backref('entities', lazy='dynamic')) inbound = db.relationship('Relation', lazy='dynamic', backref='target', primaryjoin='Entity.id==Relation.target_id') outbound = db.relationship('Relation', lazy='dynamic',
class Entity(db.Model, UUIDBase, PropertyBase): __tablename__ = 'entity' PropertyClass = EntityProperty same_as = db.Column(db.Unicode, db.ForeignKey('entity.id'), nullable=True) project_id = db.Column(db.Integer, db.ForeignKey('project.id')) author_id = db.Column(db.Integer, db.ForeignKey('account.id')) schemata = db.relationship('Schema', secondary=entity_schema, backref=db.backref('entities', lazy='dynamic')) inbound = db.relationship('Relation', lazy='dynamic', backref='target', primaryjoin='Entity.id==Relation.target_id') outbound = db.relationship('Relation', lazy='dynamic', backref='source', primaryjoin='Entity.id==Relation.source_id') properties = db.relationship(EntityProperty, backref='entity', order_by=EntityProperty.created_at.desc(), lazy='joined') @property def names(self): return [p for p in self.properties if p.name == 'name'] @classmethod def by_name(cls, project, name, only_active=False): q = db.session.query(cls) a = q.filter(cls.project == project) q = cls._filter_property(q, 'name', name, only_active=only_active) return q.first() @property def inbound_schemata(self): from grano.model.relation import Relation q = db.session.query(Schema) q = q.join(Schema.relations) q = q.filter(Relation.target_id == self.id) return q.distinct() def inbound_by_schema(self, schema): q = self.inbound.filter_by(schema=schema) return q @property def outbound_schemata(self): from grano.model.relation import Relation q = db.session.query(Schema) q = q.join(Schema.relations) q = q.filter(Relation.source_id == self.id) return q.distinct() def outbound_by_schema(self, schema): q = self.outbound.filter_by(schema=schema) return q @property def degree(self): return self.inbound.count() + self.outbound.count()
class Query(db.Model): """ A query object is a stored raw query to be run against the network to generate a report. """ __tablename__ = 'query' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode) label = db.Column(db.Unicode) query = db.Column(db.Unicode) network_id = db.Column(db.Integer, db.ForeignKey('network.id')) network = db.relationship('Network', backref=db.backref('queries', lazy='dynamic')) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) @classmethod def create(cls, network, data): obj = cls() obj.network = network obj.update(data) return obj def update(self, data): self.name = str(data.get('name')) self.label = data.get('label') self.query = data.get('query') db.session.add(self) db.session.flush() def delete(self): db.session.delete(self) def as_dict(self): return { 'id': self.id, 'name': self.name, 'label': self.label, 'query': self.query } def run(self, **kw): conn = safe_engine().connect() for rs in chain(self.network.relation_schemata, self.network.entity_schemata): q = rs.cls.view() conn.execute(text(str(q)), current=True) return conn.execute(text(self.query), **kw) @classmethod def by_name(self, network, name): q = db.session.query(Query) q = q.filter_by(network=network) q = q.filter_by(name=name) return q.first() @classmethod def all(self, network): q = db.session.query(Query) q = q.filter_by(network=network) return q def __repr__(self): return "<Query(%s,%s)>" % (self.id, self.name)
class Schema(db.Model, IntBase): __tablename__ = 'grano_schema' name = db.Column(db.Unicode()) label = db.Column(db.Unicode()) hidden = db.Column(db.Boolean()) obj = db.Column(db.Unicode()) meta = db.Column(MutableDict.as_mutable(JSONEncodedDict)) attributes = db.relationship(Attribute, backref='schema', lazy='dynamic', cascade='all, delete, delete-orphan') properties = db.relationship(Property, backref='schema', lazy='dynamic', cascade='all, delete, delete-orphan') relations = db.relationship('Relation', backref='schema', lazy='dynamic', cascade='all, delete, delete-orphan') project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) def get_attribute(self, name): for attribute in self.attributes: if attribute.name == name: return attribute @classmethod def by_name(cls, project, name): q = db.session.query(cls).filter_by(name=name) q = q.filter_by(project=project) return q.first() @classmethod def by_obj_name(cls, project, obj, name): q = db.session.query(cls) q = q.filter_by(project=project) q = q.filter_by(name=name) q = q.filter_by(obj=obj) return q.first() def to_dict_index(self): return { 'name': self.name, 'default': self.name == ENTITY_DEFAULT_SCHEMA, 'label': self.label, 'hidden': self.hidden, 'meta': self.meta, 'obj': self.obj, 'api_url': url_for('schemata_api.view', slug=self.project.slug, name=self.name) } def to_dict(self): data = self.to_dict_index() data['id'] = self.id data['project'] = self.project.to_dict_index() data['attributes'] = [a.to_dict() for a in self.attributes] return data
class Schema(db.Model, IntBase): __tablename__ = 'grano_schema' name = db.Column(db.Unicode()) label = db.Column(db.Unicode()) hidden = db.Column(db.Boolean()) obj = db.Column(db.Unicode()) meta = db.Column(MutableDict.as_mutable(JSONEncodedDict)) project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) parent_id = db.Column(db.Integer, db.ForeignKey('grano_schema.id'), nullable=True) local_attributes = db.relationship(Attribute, backref='schema', lazy='dynamic', cascade='all, delete, delete-orphan') relations = db.relationship('Relation', backref='schema', lazy='dynamic', cascade='all, delete, delete-orphan') entities = db.relationship('Entity', backref='schema', lazy='dynamic', cascade='all, delete, delete-orphan') children = db.relationship('Schema', lazy='dynamic', backref=db.backref('parent', remote_side='Schema.id')) @property def inherited_attributes(self): if self.parent is None: return [] return self.parent.attributes @property def attributes(self): return list(self.local_attributes) + self.inherited_attributes def get_attribute(self, name): for attribute in self.attributes: if attribute.name == name: return attribute def is_circular(self, path=None): if path is None: path = [] if self.name in path: return True elif self.parent is None: return False else: path.append(self.name) return self.parent.is_circular(path) def is_parent(self, other): if self.parent is None: return False if self.parent == other: return True return self.parent.is_parent(other) def common_parent(self, other): if self == other or self.is_parent(other): return self return self.common_parent(other.parent) @classmethod def by_name(cls, project, name): q = db.session.query(cls).filter_by(name=name) q = q.filter_by(project=project) return q.first() @classmethod def by_obj_name(cls, project, obj, name): q = db.session.query(cls) q = q.filter_by(project=project) q = q.filter_by(name=name) q = q.filter_by(obj=obj) return q.first() def to_dict_index(self): return { 'name': self.name, 'label': self.label, 'hidden': self.hidden, 'obj': self.obj, 'api_url': url_for('schemata_api.view', slug=self.project.slug, name=self.name) } def to_dict(self): data = self.to_dict_index() data['id'] = self.id data['meta'] = self.meta if self.parent is not None: data['parent'] = self.parent.to_dict_index() else: data['parent'] = None data['project'] = self.project.to_dict_short() data['attributes'] = [a.to_dict() for a in self.local_attributes] for attr in self.inherited_attributes: d = attr.to_dict() d['inherited'] = True data['attributes'].append(d) return data
class Schema(db.Model): """ A schema defines a specific subtype of either an entity or a relation. This can mean any graph element, such as a person, company or other actor type for entities - or a type of social, economic or political link for a relation (e.g. ownership, school attendance, ..). A schema is defined through a model structure that contains necessary metadata to handle the schema both internally and via the user interface. """ __tablename__ = 'schema' ENTITY = 'entity' RELATION = 'relation' TYPES = [ENTITY, RELATION] id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode) label = db.Column(db.Unicode) entity = db.Column(db.Unicode) network_id = db.Column(db.Integer, db.ForeignKey('network.id')) network = db.relationship('Network', backref=db.backref('schemata', lazy='dynamic')) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, onupdate=datetime.utcnow) @classmethod def create(cls, network, entity, data): obj = cls() obj.update(network, entity, data) return obj def update(self, network, entity, data): self.network = network self.name = str(data.get('name')) self.label = data.get('label') self.entity = entity db.session.add(self) db.session.flush() attributes = data.get('attributes', {}) for attribute in self.attributes: if attribute.name in attributes: del attributes[attribute.name] else: attribute.delete() for name, data in attributes.items(): attr = Attribute.create(name, data) self.attributes.append(attr) db.session.flush() self.migrate() def migrate(self): self._make_cls() table = self._cls.__table__ if not table.exists(db.engine): table.create(db.engine) else: table.metadata.bind = db.engine for attribute in self.attributes: try: col = table.c[attribute.name] col.create() except Exception as e: #log.exception(e) pass def delete(self): for attribute in self.attributes: attribute.delete() db.session.delete(self) @property def cls(self): if not hasattr(self, '_cls'): self._make_cls() return self._cls @property def parent_cls(self): return { self.ENTITY: self.network.Entity, self.RELATION: self.network.Relation, }.get(self.entity) def _make_cls(self): """ Generate a new type, mapped through SQLAlchemy. This will be a joined subtable to either an entity or a relation and retain a copy of its composite primary key plus any attributes defined in the schema. """ prefix = self.parent_cls.__tablename__ # inherit primary key: cls = { '__tablename__': prefix + '__' + self.name, 'id': db.Column(db.String(36), primary_key=True), 'serial': db.Column(db.BigInteger, primary_key=True) } # set up inheritance: cls['__mapper_args__'] = { 'polymorphic_identity': self.name, 'inherit_condition': db.and_(cls['id'] == self.parent_cls.id, cls['serial'] == self.parent_cls.serial) } cls['__table_args__'] = {'extend_existing': True} # set up the specific attributes: for attribute in self.attributes: cls[attribute.name] = attribute.column # make an as_dict method: def as_dict(ins): d = self.parent_cls.as_dict(ins) for attribute in self.attributes: d[attribute.name] = \ getattr(ins, attribute.name) return d cls['as_dict'] = as_dict self._cls = type(str(self.name), (self.parent_cls, ), cls) def as_dict(self): attrs = [(a.name, a.as_dict()) for a in self.attributes] return { 'id': self.id, 'name': self.name, 'label': self.label, 'entity': self.entity, 'attributes': dict(attrs) } def __repr__(self): return "<Schema(%s,%s)>" % (self.id, self.name)
class Entity(db.Model, UUIDBase, PropertyBase): __tablename__ = 'grano_entity' same_as = db.Column(db.Unicode, db.ForeignKey('grano_entity.id'), nullable=True) project_id = db.Column(db.Integer, db.ForeignKey('grano_project.id')) author_id = db.Column(db.Integer, db.ForeignKey('grano_account.id')) schemata = db.relationship('Schema', secondary=entity_schema, backref=db.backref('entities', lazy='dynamic')) inbound = db.relationship('Relation', lazy='dynamic', backref='target', primaryjoin='Entity.id==Relation.target_id', cascade='all, delete, delete-orphan') outbound = db.relationship('Relation', lazy='dynamic', backref='source', primaryjoin='Entity.id==Relation.source_id', cascade='all, delete, delete-orphan') properties = db.relationship(Property, backref='entity', order_by=Property.created_at.desc(), cascade='all, delete, delete-orphan', lazy='joined') status = db.Column(db.Integer, default=0) @property def names(self): return [p for p in self.properties if p.name == 'name'] @classmethod def by_name(cls, project, name, only_active=False): q = cls.by_name_many(project, name, only_active=only_active) return q.first() @classmethod def by_name_many(cls, project, name, only_active=False): q = db.session.query(cls) q = q.filter(cls.project == project) attr = project.get_attribute('entity', 'name') q = cls._filter_property(q, [attr], name, only_active=only_active) return q @classmethod def by_id_many(cls, ids, account=None): from grano.model import Project, Permission q = db.session.query(cls) q = q.filter(cls.id.in_(ids)) if account is not None: q = q.join(Project) q = q.outerjoin(Permission) q = q.filter( or_( Project.private == False, # noqa and_(Permission.reader == True, Permission.account == account))) id_map = {} for e in q.all(): id_map[e.id] = e return id_map @property def inbound_schemata(self): from grano.model.relation import Relation q = db.session.query(Schema) q = q.join(Schema.relations) q = q.filter(Relation.target_id == self.id) return q.distinct() def inbound_by_schema(self, schema): q = self.inbound.filter_by(schema=schema) return q @property def outbound_schemata(self): from grano.model.relation import Relation q = db.session.query(Schema) q = q.join(Schema.relations) q = q.filter(Relation.source_id == self.id) return q.distinct() def outbound_by_schema(self, schema): q = self.outbound.filter_by(schema=schema) return q @property def degree(self): return self.inbound.count() + self.outbound.count() def to_dict_index(self): """ Convert an entity to the REST API form. """ data = { 'id': self.id, 'status': self.status, 'project': self.project.to_dict_index(), 'api_url': url_for('entities_api.view', id=self.id), 'properties': {} } data['schemata'] = [s.to_dict_index() for s in self.schemata] for prop in self.active_properties: name, prop = prop.to_dict_kv() data['properties'][name] = prop if self.same_as: data['same_as'] = self.same_as data['same_as_url'] = url_for('entities_api.view', id=self.same_as) return data def to_dict(self): """ Full serialization of the entity. """ data = self.to_dict_index() data['created_at'] = self.created_at data['updated_at'] = self.updated_at data['inbound_relations'] = self.inbound.count() if data['inbound_relations'] > 0: data['inbound_url'] = url_for('relations_api.index', target=self.id) data['outbound_relations'] = self.outbound.count() if data['outbound_relations'] > 0: data['outbound_url'] = url_for('relations_api.index', source=self.id) return data def to_index(self): """ Convert an entity to a form appropriate for search indexing. """ data = self.to_dict() data['degree'] = self.degree data['names'] = [] for prop in self.properties: if prop.name == 'name': data['names'].append(prop.value) return data