Skip to content

Commit

Permalink
Re-organize api objects initialization to avoid cyclic imports
Browse files Browse the repository at this point in the history
Api database object (flask-sqlalchemy) is initialized in a separate
module so that it can be imported by other api modules without causing
a cyclic import between the app flask object and the api module itself.
  • Loading branch information
ppinatti committed Feb 13, 2018
1 parent 9339c54 commit b315f03
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 88 deletions.
90 changes: 32 additions & 58 deletions tessia/server/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# limitations under the License.

"""
Configuration of API objects
Initialization of API objects
"""

#
Expand All @@ -23,11 +23,14 @@
from flask_potion import Api
from flask_potion import exceptions as potion_exceptions
from tessia.server.config import CONF
from tessia.server.api import exceptions as api_exceptions
from tessia.server.api.db import API_DB
from tessia.server.api.manager import ApiManager
from tessia.server.api.resources import RESOURCES
from tessia.server.api.views import version
from tessia.server.api.views.auth import authorize
from tessia.server.db.connection import MANAGER
from tessia.server.db.models import BASE

# used by potion to connect the Resources with the Models
import flask_sqlalchemy as flask_sa
import logging

#
Expand All @@ -51,11 +54,11 @@ class _AppManager(object):

def __init__(self):
"""
Constructor, defines the variable that stores the app and db objects
instances as empty. The app creation and db configuration are triggered
on the first time one of the variables app or db is referenced.
Constructor, defines the variable that stores the app object instance
as empty. The app creation is triggered on the first time the variable
'app' is referenced.
"""
self._api = None
self._app = None
# __init__()

def __new__(cls, *args, **kwargs):
Expand Down Expand Up @@ -83,19 +86,19 @@ def _configure(self):
"""
Perform all necessary configuration steps to prepare the rest api.
"""
if self._api is not None:
if self._app is not None:
return

# create the flask app instance
app = self._create_app()

# create the flask-sa object
database = self._create_db(app)
# initialize the flask-sa object
self._init_db(app)

# create the api entry points
self._create_api(app)

self._api = (app, database)
self._app = app
# _configure()

def _create_api(self, app):
Expand All @@ -111,13 +114,6 @@ def _create_api(self, app):
Raises:
RuntimeError: in case a resource is missing mandatory attribute
"""
# these imports are made here to avoid circular import problems
from tessia.server.api.manager import ApiManager
from tessia.server.api.resources import RESOURCES
from tessia.server.api.views.auth import authorize
from tessia.server.api.views import version
from tessia.server.api import exceptions as api_exceptions

# version verification routine when defined by the client in headers
app.before_request(version.check_version)
# add the api version header on each response
Expand Down Expand Up @@ -209,62 +205,40 @@ def _create_app():
return app
# _create_app()

def _create_db(self, app):
@staticmethod
def _init_db(app):
"""
Create the flask-sqlalchemy instance for db communication
Init the app in the flask-sqlalchemy instance for db communication
Args:
app (Flask): flask object
Returns:
SQLAlchemy: instance of flask-SQLAlchemy
Raises:
RuntimeError: in case db url is not found in cfg file
"""
def patched_base(self, *args, **kwargs):
"""
Change the flask_sqlalchemy base creator function to use our custom
declarative base in place of the default one.
"""
# add our base to the query property of each model we have
# in case a query property was already added by the db.connection
# module it will be overriden here, which is ok because the
# flask_sa implementation just add a few bits more like pagination
for cls_model in BASE._decl_class_registry.values():
if isinstance(cls_model, type):
cls_model.query_class = flask_sa.BaseQuery
cls_model.query = flask_sa._QueryProperty(self)

# return our base as the base to be used by flask-sa
return BASE
# patched_base()

app.config['SQLALCHEMY_DATABASE_URI'] = MANAGER.engine.url
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
flask_sa.SQLAlchemy.make_declarative_base = patched_base
flask_sa.SQLAlchemy.create_session = lambda *args, **kwargs: \
MANAGER.session

return flask_sa.SQLAlchemy(app, model_class=BASE)
# _create_db()
API_DB.db.init_app(app)
# _init_db()

@property
def app(self):
"""
Return the flask's application instance
"""
self._configure()
return self._api[0]
return self._app
# app

@property
def db(self):
def reset(self):
"""
Return the flask-sa's db object
Force recreation of app and db objects. This method is primarily
targeted for unit tests.
"""
self._configure()
return self._api[1]

self._app = None
API_DB._db = None
# the potion resource might be tied to a previous instance so we remove
# the association
for resource in RESOURCES:
resource.api = None
# reset()
# _AppManager

API = _AppManager()
116 changes: 116 additions & 0 deletions tessia/server/api/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Copyright 2018 IBM Corp.
#
# 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.

"""
Configuration of flask-sqlalchemy for use by flask app
"""

#
# IMPORTS
#
from tessia.server.db.connection import MANAGER
from tessia.server.db.models import BASE

import flask_sqlalchemy as flask_sa

#
# CONSTANTS AND DEFINITIONS
#

#
# CODE
#

class _AppDbManager(object):
"""
Class to handle db object creation and configuration
"""
_singleton = False

def __init__(self):
"""
Constructor, defines the variable that stores the db object instance
as empty. The db initialization is triggered on the first time the
variable 'db' is referenced.
"""
self._db = None
# __init__()

def __new__(cls, *args, **kwargs):
"""
Modules should not instantiate this class since there should be only
one db entry point at a time for all modules.
Args:
None
Returns:
_AppDbManager: object instance
Raises:
NotImplementedError: as the class should not be instantiated
"""
if cls._singleton:
raise NotImplementedError('Class should not be instantiated')
cls._singleton = True

return super().__new__(cls, *args, **kwargs)
# __new__()

def _create_db(self):
"""
Create the flask-sqlalchemy instance for db communication
Returns:
SQLAlchemy: instance of flask-SQLAlchemy
"""
def patched_base(self, *args, **kwargs):
"""
Change the flask_sqlalchemy base creator function to use our custom
declarative base in place of the default one.
"""
# add our base to the query property of each model we have
# in case a query property was already added by the db.connection
# module it will be overriden here, which is ok because the
# flask_sa implementation just add a few bits more like pagination
for cls_model in BASE._decl_class_registry.values():
if isinstance(cls_model, type):
cls_model.query_class = flask_sa.BaseQuery
cls_model.query = flask_sa._QueryProperty(self)

# return our base as the base to be used by flask-sa
return BASE
# patched_base()

flask_sa.SQLAlchemy.make_declarative_base = patched_base
flask_sa.SQLAlchemy.create_session = lambda *args, **kwargs: \
MANAGER.session

return flask_sa.SQLAlchemy(model_class=BASE)
# _create_db()

@property
def db(self):
"""
Return the flask-sa's db object
"""
if self._db is not None:
return self._db

self._db = self._create_db()
return self._db
# db
# _AppDbManager

API_DB = _AppDbManager()
22 changes: 11 additions & 11 deletions tessia/server/api/resources/system_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from flask_potion.contrib.alchemy.fields import InlineModel
from flask_potion.instances import Pagination
from sqlalchemy.exc import IntegrityError
from tessia.server.api.app import API
from tessia.server.api.db import API_DB
from tessia.server.api.exceptions import BaseHttpError
from tessia.server.api.exceptions import ConflictError
from tessia.server.api.exceptions import ItemNotFoundError
Expand Down Expand Up @@ -257,7 +257,7 @@ def do_create(self, properties):
def_profile.default = False
# do not commit yet, let the manager do it when updating the
# target profile to make it an atomic operation
API.db.session.add(def_profile)
API_DB.db.session.add(def_profile)

hyp_prof_name = properties.get('hypervisor_profile')
if hyp_prof_name is not None:
Expand Down Expand Up @@ -431,7 +431,7 @@ def do_update(self, properties, id):
def_profile.default = False
# do not commit yet, let the manager do it when updating the
# target profile to make it an atomic operation
API.db.session.add(def_profile)
API_DB.db.session.add(def_profile)
# a profile cannot unset its default flag otherwise we would have a
# state where a system has no default profile, instead it has to be
# replaced by another
Expand Down Expand Up @@ -502,9 +502,9 @@ def attach_storage_volume(self, properties, id):
# create association
new_attach = StorageVolumeProfileAssociation(
profile_id=id, volume_id=properties['unique_id'])
API.db.session.add(new_attach)
API_DB.db.session.add(new_attach)
try:
API.db.session.commit()
API_DB.db.session.commit()
# duplicate entry
except IntegrityError as exc:
raise ConflictError(exc, None)
Expand Down Expand Up @@ -538,7 +538,7 @@ def detach_storage_volume(self, id, vol_unique_id):
# TODO: create a schema to have human-readable content in the error
# message
raise ItemNotFoundError('profile_id,volume_id', value, None)
API.db.session.delete(match)
API_DB.db.session.delete(match)

last = StorageVolumeProfileAssociation.query.filter_by(
volume_id=vol_unique_id,
Expand All @@ -548,7 +548,7 @@ def detach_storage_volume(self, id, vol_unique_id):
StorageVolume.query.filter_by(id=vol_unique_id).update(
{'system_id': None})

API.db.session.commit()
API_DB.db.session.commit()
return True
# detach_storage_volume
detach_storage_volume.request_schema = None
Expand All @@ -574,9 +574,9 @@ def attach_iface(self, properties, id):
# create association
new_attach = SystemIfaceProfileAssociation(
profile_id=id, iface_id=properties['id'])
API.db.session.add(new_attach)
API_DB.db.session.add(new_attach)
try:
API.db.session.commit()
API_DB.db.session.commit()
# duplicate entry
except IntegrityError as exc:
raise ConflictError(exc, None)
Expand Down Expand Up @@ -610,9 +610,9 @@ def detach_iface(self, id, iface_id):
# TODO: create a schema to have human-readable content in the error
# message
raise ItemNotFoundError('profile_id,iface_id', value, None)
API.db.session.delete(match)
API_DB.db.session.delete(match)

API.db.session.commit()
API_DB.db.session.commit()
return True
# detach_iface
detach_iface.request_schema = None
Expand Down
Loading

0 comments on commit b315f03

Please sign in to comment.