235 lines
6.7 KiB
Python
235 lines
6.7 KiB
Python
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import functools
|
|
import json
|
|
import re
|
|
|
|
import flask_sqlalchemy
|
|
import sqlalchemy.event
|
|
import sqlalchemy.ext.declarative as sa_decl
|
|
from sqlalchemy import types
|
|
|
|
try:
|
|
from importlib import reload
|
|
except ImportError:
|
|
pass # in 2.x reload is builtin
|
|
|
|
if not hasattr(flask_sqlalchemy.BaseQuery, 'one_or_none'):
|
|
# for sqlalchemy < 1.0.9
|
|
from sqlalchemy.orm import exc as orm_exc
|
|
|
|
def one_or_none(self):
|
|
ret = list(self)
|
|
l = len(ret)
|
|
if l == 1:
|
|
return ret[0]
|
|
elif l == 0:
|
|
return None
|
|
else:
|
|
raise orm_exc.MultipleResultsFound(
|
|
"Multiple rows were found for one_or_none()")
|
|
flask_sqlalchemy.BaseQuery.one_or_none = one_or_none
|
|
|
|
db = flask_sqlalchemy.SQLAlchemy()
|
|
pk_type = db.Integer
|
|
pk = functools.partial(db.Column, pk_type, primary_key=True)
|
|
|
|
|
|
def fk(cls, **kwargs):
|
|
return db.Column(pk_type, db.ForeignKey(cls.id), **kwargs)
|
|
|
|
|
|
def _tablename(cls_name):
|
|
def repl(match):
|
|
res = match.group().lower()
|
|
if match.start():
|
|
res = "_" + res
|
|
return res
|
|
|
|
return ModelMixin.table_prefix + re.sub("[A-Z]", repl, cls_name)
|
|
|
|
|
|
class ModelMixin(object):
|
|
id = db.Column(pk_type, primary_key=True)
|
|
|
|
try:
|
|
table_prefix = ModelMixin.table_prefix # keep prefix during reload
|
|
except NameError:
|
|
table_prefix = "" # first import, not reload
|
|
|
|
@sa_decl.declared_attr
|
|
def __tablename__(cls):
|
|
return _tablename(cls.__name__)
|
|
|
|
def __repr__(self):
|
|
args = []
|
|
for attr in self.__repr_attrs__:
|
|
value = getattr(self, attr)
|
|
if attr == 'content' and value is not None and len(value) > 15:
|
|
value = value[:10] + '<...>'
|
|
args.append('{}={!r}'.format(attr, value))
|
|
return '{}({})'.format(type(self).__name__, ','.join(args))
|
|
|
|
|
|
class Json(types.TypeDecorator):
|
|
impl = db.Text
|
|
|
|
def process_bind_param(self, value, dialect):
|
|
return json.dumps(value)
|
|
|
|
def process_result_value(self, value, dialect):
|
|
return json.loads(value)
|
|
|
|
|
|
class Namespace(ModelMixin, db.Model):
|
|
name = db.Column(db.String(128))
|
|
__repr_attrs__ = ('id', 'name')
|
|
|
|
# Component registry
|
|
|
|
|
|
class Component(ModelMixin, db.Model):
|
|
name = db.Column(db.String(128))
|
|
|
|
__repr_attrs__ = ('id', 'name')
|
|
|
|
|
|
class Schema(ModelMixin, db.Model):
|
|
name = db.Column(db.String(128))
|
|
component_id = fk(Component)
|
|
component = db.relationship(Component, backref='schemas')
|
|
namespace_id = fk(Namespace)
|
|
namespace = db.relationship(Namespace)
|
|
content = db.Column(Json)
|
|
|
|
__repr_attrs__ = ('id', 'name', 'component', 'namespace', 'content')
|
|
|
|
|
|
class Template(ModelMixin, db.Model):
|
|
name = db.Column(db.String(128))
|
|
component_id = fk(Component)
|
|
component = db.relationship(Component, backref='templates')
|
|
content = db.Column(Json)
|
|
|
|
__repr_attrs__ = ('id', 'name', 'component', 'content')
|
|
|
|
# Environment data storage
|
|
|
|
|
|
class Environment(ModelMixin, db.Model):
|
|
@sa_decl.declared_attr
|
|
def components(cls):
|
|
_environment_components = db.Table(
|
|
_tablename('environment_components'),
|
|
db.Column('environment_id', pk_type, db.ForeignKey(cls.id)),
|
|
db.Column('component_id', pk_type, db.ForeignKey(Component.id)),
|
|
)
|
|
return db.relationship(Component, secondary=_environment_components)
|
|
|
|
__repr_attrs__ = ('id',)
|
|
|
|
|
|
class EnvironmentHierarchyLevel(ModelMixin, db.Model):
|
|
environment_id = fk(Environment)
|
|
environment = db.relationship(Environment, backref='hierarchy_levels')
|
|
name = db.Column(db.String(128))
|
|
|
|
@sa_decl.declared_attr
|
|
def parent_id(cls):
|
|
return db.Column(pk_type, db.ForeignKey(cls.id))
|
|
|
|
@sa_decl.declared_attr
|
|
def parent(cls):
|
|
return db.relationship(cls,
|
|
backref=db.backref('child', uselist=False),
|
|
remote_side=cls.id)
|
|
|
|
__table_args__ = (
|
|
db.UniqueConstraint('environment_id', 'name'),
|
|
db.UniqueConstraint('environment_id', 'parent_id'),
|
|
)
|
|
__repr_attrs__ = ('id', 'environment', 'parent', 'name')
|
|
|
|
@classmethod
|
|
def get_for_environment(cls, environment):
|
|
query = cls.query.filter_by(environment=environment, parent=None)
|
|
root_level = query.one_or_none()
|
|
if not root_level:
|
|
return []
|
|
env_levels = [root_level]
|
|
while env_levels[-1].child:
|
|
env_levels.append(env_levels[-1].child)
|
|
return env_levels
|
|
|
|
|
|
class EnvironmentHierarchyLevelValue(ModelMixin, db.Model):
|
|
level_id = fk(EnvironmentHierarchyLevel)
|
|
level = db.relationship(EnvironmentHierarchyLevel)
|
|
value = db.Column(db.String(128))
|
|
|
|
@sa_decl.declared_attr
|
|
def parent_id(cls):
|
|
return db.Column(pk_type, db.ForeignKey(cls.id))
|
|
|
|
@sa_decl.declared_attr
|
|
def parent(cls):
|
|
return db.relationship(cls, remote_side=cls.id)
|
|
|
|
__repr_attrs__ = ('id', 'level', 'parent', 'value')
|
|
|
|
|
|
class EnvironmentSchemaValues(ModelMixin, db.Model):
|
|
environment_id = fk(Environment)
|
|
environment = db.relationship(Environment)
|
|
schema_id = fk(Schema)
|
|
schema = db.relationship(Schema)
|
|
level_value_id = fk(EnvironmentHierarchyLevelValue)
|
|
level_value = db.relationship('EnvironmentHierarchyLevelValue')
|
|
values = db.Column(Json)
|
|
|
|
__table_args__ = (
|
|
db.UniqueConstraint(environment_id, schema_id, level_value_id),
|
|
)
|
|
__repr_attrs__ = ('id', 'environment', 'schema', 'level_value', 'values')
|
|
|
|
|
|
def get_or_create(cls, **attrs):
|
|
with db.session.begin(nested=True):
|
|
item = cls.query.filter_by(**attrs).one_or_none()
|
|
if not item:
|
|
item = cls(**attrs)
|
|
db.session.add(item)
|
|
return item
|
|
|
|
|
|
def fix_sqlite():
|
|
engine = db.engine
|
|
|
|
@sqlalchemy.event.listens_for(engine, "connect")
|
|
def _connect(dbapi_connection, connection_record):
|
|
dbapi_connection.isolation_level = None
|
|
|
|
@sqlalchemy.event.listens_for(engine, "begin")
|
|
def _begin(conn):
|
|
conn.execute("BEGIN")
|
|
|
|
|
|
def prefix_tables(module, prefix):
|
|
ModelMixin.table_prefix = prefix
|
|
reload(module)
|
|
|
|
|
|
def unprefix_tables(module):
|
|
ModelMixin.table_prefix = ""
|
|
reload(module)
|