Alexander Kislitsky 8f5bf1883d Put, post operations implemented for environments
Implementation of environment operations moved to library package.
Code for searching objects by ids and names extracted to the library
function.
Exceptions propagation added to example config. It is useful for
testing and troubleshooting to have error message not only in logs
but in the API response too.
Tuningbox errors hierarchy added to the project.

Change-Id: Ic2fd3c3c17409723bfa3cfff1c0bb18f3a65f0d7
2016-08-15 18:54:06 +03:00

253 lines
7.4 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
import flask_sqlalchemy
import sqlalchemy.event
import sqlalchemy.ext.declarative as sa_decl
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy import types
try:
from importlib import reload
except ImportError:
pass # in 2.x reload is builtin
db = flask_sqlalchemy.SQLAlchemy(session_options={'autocommit': True})
pk_type = db.Integer
pk = functools.partial(db.Column, pk_type, primary_key=True)
def with_transaction(f):
@functools.wraps(f)
def inner(*args, **kwargs):
with db.session.begin():
return f(*args, **kwargs)
return inner
def fk(cls, **kwargs):
ondelete = kwargs.pop('ondelete', None)
return db.Column(pk_type, db.ForeignKey(cls.id, ondelete=ondelete),
**kwargs)
class BaseQuery(flask_sqlalchemy.BaseQuery):
def get_by_id_or_name(self, id_or_name, fail_on_none=True):
if isinstance(id_or_name, int):
result = self.get(id_or_name)
else:
result = self.filter_by(name=id_or_name).one_or_none()
if fail_on_none and result is None:
flask.abort(404)
return result
# one_or_none is not present in sqlalchemy < 1.0.9
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()")
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):
query_class = BaseQuery
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)
# Component registry
class Component(ModelMixin, db.Model):
name = db.Column(db.String(128), unique=True)
__repr_attrs__ = ('id', 'name')
class ResourceDefinition(ModelMixin, db.Model):
name = db.Column(db.String(128))
component_id = fk(Component, ondelete='CASCADE')
component = db.relationship(Component, backref='resource_definitions')
content = db.Column(Json)
__repr_attrs__ = ('id', 'name', 'component', 'content')
# Environment data storage
class Environment(ModelMixin, db.Model):
@sa_decl.declared_attr
def environment_components_table(cls):
return db.Table(
_tablename('environment_components'),
db.Column('environment_id', pk_type,
db.ForeignKey(cls.id, ondelete='CASCADE')),
db.Column('component_id', pk_type,
db.ForeignKey(Component.id, ondelete='CASCADE')),
)
@sa_decl.declared_attr
def components(cls):
return db.relationship(
Component, secondary=cls.environment_components_table)
__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)
# TODO(yorik-sar): add UniqueConstraint for all fields
__repr_attrs__ = ('id', 'level', 'parent', 'value')
class ResourceValues(ModelMixin, db.Model):
environment_id = fk(Environment, ondelete='CASCADE')
environment = db.relationship(Environment)
resource_definition_id = fk(ResourceDefinition, ondelete='CASCADE')
resource_definition = db.relationship(ResourceDefinition)
level_value_id = fk(EnvironmentHierarchyLevelValue, ondelete='CASCADE')
level_value = db.relationship('EnvironmentHierarchyLevelValue')
values = db.Column(Json, server_default='{}')
overrides = db.Column(Json, server_default='{}')
__table_args__ = (
db.UniqueConstraint(environment_id, resource_definition_id,
level_value_id),
)
__repr_attrs__ = ('id', 'environment', 'resource_definition',
'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)
# TODO(yorik-sar): handle constraints failure in case of
# race condition
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)