Skip to content

Commit

Permalink
chore(dao/command): Add transaction decorator to try to enforce "unit…
Browse files Browse the repository at this point in the history
… of work" (#24969)
  • Loading branch information
john-bodley authored and eschutho committed Jul 24, 2024
1 parent 0dca1d4 commit e148327
Show file tree
Hide file tree
Showing 151 changed files with 682 additions and 917 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ ignore_basepython_conflict = true
commands =
superset db upgrade
superset init
superset load-test-users
# use -s to be able to use break pointers.
# no args or tests/* can be passed as an argument to run all tests
pytest -s {posargs}
Expand Down
7 changes: 2 additions & 5 deletions scripts/permissions_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from collections import defaultdict

from superset import security_manager
from superset.utils.decorators import transaction


@transaction()
def cleanup_permissions() -> None:
# 1. Clean up duplicates.
pvms = security_manager.get_session.query(
Expand All @@ -29,7 +31,6 @@ def cleanup_permissions() -> None:
for pvm in pvms:
pvms_dict[(pvm.permission, pvm.view_menu)].append(pvm)
duplicates = [v for v in pvms_dict.values() if len(v) > 1]
len(duplicates)

for pvm_list in duplicates:
first_prm = pvm_list[0]
Expand All @@ -38,7 +39,6 @@ def cleanup_permissions() -> None:
roles = roles.union(pvm.role)
security_manager.get_session.delete(pvm)
first_prm.roles = list(roles)
security_manager.get_session.commit()

pvms = security_manager.get_session.query(
security_manager.permissionview_model
Expand All @@ -52,7 +52,6 @@ def cleanup_permissions() -> None:
for pvm in pvms:
if not (pvm.view_menu and pvm.permission):
security_manager.get_session.delete(pvm)
security_manager.get_session.commit()

pvms = security_manager.get_session.query(
security_manager.permissionview_model
Expand All @@ -63,15 +62,13 @@ def cleanup_permissions() -> None:
roles = security_manager.get_session.query(security_manager.role_model).all()
for role in roles:
role.permissions = [p for p in role.permissions if p]
security_manager.get_session.commit()

# 4. Delete empty roles from permission view menus
pvms = security_manager.get_session.query(
security_manager.permissionview_model
).all()
for pvm in pvms:
pvm.role = [r for r in pvm.role if r]
security_manager.get_session.commit()


cleanup_permissions()
1 change: 1 addition & 0 deletions scripts/python_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ echo "Superset config module: $SUPERSET_CONFIG"

superset db upgrade
superset init
superset load-test-users

echo "Running tests"

Expand Down
8 changes: 4 additions & 4 deletions superset/cachekeys/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ def invalidate(self) -> Response:
delete_stmt = CacheKey.__table__.delete().where( # pylint: disable=no-member
CacheKey.cache_key.in_(cache_keys)
)
db.session.execute(delete_stmt)
db.session.commit()

with db.session.begin_nested():
db.session.execute(delete_stmt)

stats_logger_manager.instance.gauge(
"invalidated_cache", len(cache_keys)
)
Expand All @@ -125,7 +127,5 @@ def invalidate(self) -> Response:
)
except SQLAlchemyError as ex: # pragma: no cover
logger.error(ex, exc_info=True)
db.session.rollback()
return self.response_500(str(ex))
db.session.commit()
return self.response(201)
2 changes: 2 additions & 0 deletions superset/cli/examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from flask.cli import with_appcontext

import superset.utils.database as database_utils
from superset.utils.decorators import transaction

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -89,6 +90,7 @@ def load_examples_run(

@click.command()
@with_appcontext
@transaction()
@click.option("--load-test-data", "-t", is_flag=True, help="Load additional test data")
@click.option("--load-big-data", "-b", is_flag=True, help="Load additional big data")
@click.option(
Expand Down
2 changes: 2 additions & 0 deletions superset/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from superset import app, appbuilder, cli, security_manager
from superset.cli.lib import normalize_token
from superset.extensions import db
from superset.utils.decorators import transaction

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -60,6 +61,7 @@ def make_shell_context() -> dict[str, Any]:

@superset.command()
@with_appcontext
@transaction()
def init() -> None:
"""Inits the Superset application"""
appbuilder.add_permissions(update_perms=True)
Expand Down
11 changes: 2 additions & 9 deletions superset/cli/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,22 @@

import superset.utils.database as database_utils
from superset import app, security_manager
from superset.utils.decorators import transaction

logger = logging.getLogger(__name__)


@click.command()
@with_appcontext
@transaction()
def load_test_users() -> None:
"""
Loads admin, alpha, and gamma user for testing purposes
Syncs permissions for those users/roles
"""
print(Fore.GREEN + "Loading a set of users for unit tests")
load_test_users_run()


def load_test_users_run() -> None:
"""
Loads admin, alpha, and gamma user for testing purposes
Syncs permissions for those users/roles
"""
if app.config["TESTING"]:
sm = security_manager

Expand Down Expand Up @@ -84,4 +78,3 @@ def load_test_users_run() -> None:
sm.find_role(role),
password="general",
)
sm.get_session.commit()
3 changes: 3 additions & 0 deletions superset/cli/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
from flask_appbuilder.api.manager import resolver

import superset.utils.database as database_utils
from superset.utils.decorators import transaction
from superset.utils.encrypt import SecretsMigrator

logger = logging.getLogger(__name__)


@click.command()
@with_appcontext
@transaction()
@click.option("--database_name", "-d", help="Database name to change")
@click.option("--uri", "-u", help="Database URI to change")
@click.option(
Expand All @@ -53,6 +55,7 @@ def set_database_uri(database_name: str, uri: str, skip_create: bool) -> None:

@click.command()
@with_appcontext
@transaction()
def sync_tags() -> None:
"""Rebuilds special tags (owner, type, favorited by)."""
# pylint: disable=no-member
Expand Down
10 changes: 4 additions & 6 deletions superset/commands/annotation_layer/annotation/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
import logging
from datetime import datetime
from functools import partial
from typing import Any, Optional

from flask_appbuilder.models.sqla import Model
Expand All @@ -30,7 +31,7 @@
from superset.commands.annotation_layer.exceptions import AnnotationLayerNotFoundError
from superset.commands.base import BaseCommand
from superset.daos.annotation_layer import AnnotationDAO, AnnotationLayerDAO
from superset.daos.exceptions import DAOCreateFailedError
from superset.utils.decorators import on_error, transaction

logger = logging.getLogger(__name__)

Expand All @@ -39,13 +40,10 @@ class CreateAnnotationCommand(BaseCommand):
def __init__(self, data: dict[str, Any]):
self._properties = data.copy()

@transaction(on_error=partial(on_error, reraise=AnnotationCreateFailedError))
def run(self) -> Model:
self.validate()
try:
return AnnotationDAO.create(attributes=self._properties)
except DAOCreateFailedError as ex:
logger.exception(ex.exception)
raise AnnotationCreateFailedError() from ex
return AnnotationDAO.create(attributes=self._properties)

def validate(self) -> None:
exceptions: list[ValidationError] = []
Expand Down
11 changes: 4 additions & 7 deletions superset/commands/annotation_layer/annotation/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from functools import partial
from typing import Optional

from superset.commands.annotation_layer.annotation.exceptions import (
Expand All @@ -23,8 +24,8 @@
)
from superset.commands.base import BaseCommand
from superset.daos.annotation_layer import AnnotationDAO
from superset.daos.exceptions import DAODeleteFailedError
from superset.models.annotations import Annotation
from superset.utils.decorators import on_error, transaction

logger = logging.getLogger(__name__)

Expand All @@ -34,15 +35,11 @@ def __init__(self, model_ids: list[int]):
self._model_ids = model_ids
self._models: Optional[list[Annotation]] = None

@transaction(on_error=partial(on_error, reraise=AnnotationDeleteFailedError))
def run(self) -> None:
self.validate()
assert self._models

try:
AnnotationDAO.delete(self._models)
except DAODeleteFailedError as ex:
logger.exception(ex.exception)
raise AnnotationDeleteFailedError() from ex
AnnotationDAO.delete(self._models)

def validate(self) -> None:
# Validate/populate model exists
Expand Down
12 changes: 4 additions & 8 deletions superset/commands/annotation_layer/annotation/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
import logging
from datetime import datetime
from functools import partial
from typing import Any, Optional

from flask_appbuilder.models.sqla import Model
Expand All @@ -31,8 +32,8 @@
from superset.commands.annotation_layer.exceptions import AnnotationLayerNotFoundError
from superset.commands.base import BaseCommand
from superset.daos.annotation_layer import AnnotationDAO, AnnotationLayerDAO
from superset.daos.exceptions import DAOUpdateFailedError
from superset.models.annotations import Annotation
from superset.utils.decorators import on_error, transaction

logger = logging.getLogger(__name__)

Expand All @@ -43,16 +44,11 @@ def __init__(self, model_id: int, data: dict[str, Any]):
self._properties = data.copy()
self._model: Optional[Annotation] = None

@transaction(on_error=partial(on_error, reraise=AnnotationUpdateFailedError))
def run(self) -> Model:
self.validate()
assert self._model

try:
annotation = AnnotationDAO.update(self._model, self._properties)
except DAOUpdateFailedError as ex:
logger.exception(ex.exception)
raise AnnotationUpdateFailedError() from ex
return annotation
return AnnotationDAO.update(self._model, self._properties)

def validate(self) -> None:
exceptions: list[ValidationError] = []
Expand Down
10 changes: 4 additions & 6 deletions superset/commands/annotation_layer/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from functools import partial
from typing import Any

from flask_appbuilder.models.sqla import Model
Expand All @@ -27,7 +28,7 @@
)
from superset.commands.base import BaseCommand
from superset.daos.annotation_layer import AnnotationLayerDAO
from superset.daos.exceptions import DAOCreateFailedError
from superset.utils.decorators import on_error, transaction

logger = logging.getLogger(__name__)

Expand All @@ -36,13 +37,10 @@ class CreateAnnotationLayerCommand(BaseCommand):
def __init__(self, data: dict[str, Any]):
self._properties = data.copy()

@transaction(on_error=partial(on_error, reraise=AnnotationLayerCreateFailedError))
def run(self) -> Model:
self.validate()
try:
return AnnotationLayerDAO.create(attributes=self._properties)
except DAOCreateFailedError as ex:
logger.exception(ex.exception)
raise AnnotationLayerCreateFailedError() from ex
return AnnotationLayerDAO.create(attributes=self._properties)

def validate(self) -> None:
exceptions: list[ValidationError] = []
Expand Down
11 changes: 4 additions & 7 deletions superset/commands/annotation_layer/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from functools import partial
from typing import Optional

from superset.commands.annotation_layer.exceptions import (
Expand All @@ -24,8 +25,8 @@
)
from superset.commands.base import BaseCommand
from superset.daos.annotation_layer import AnnotationLayerDAO
from superset.daos.exceptions import DAODeleteFailedError
from superset.models.annotations import AnnotationLayer
from superset.utils.decorators import on_error, transaction

logger = logging.getLogger(__name__)

Expand All @@ -35,15 +36,11 @@ def __init__(self, model_ids: list[int]):
self._model_ids = model_ids
self._models: Optional[list[AnnotationLayer]] = None

@transaction(on_error=partial(on_error, reraise=AnnotationLayerDeleteFailedError))
def run(self) -> None:
self.validate()
assert self._models

try:
AnnotationLayerDAO.delete(self._models)
except DAODeleteFailedError as ex:
logger.exception(ex.exception)
raise AnnotationLayerDeleteFailedError() from ex
AnnotationLayerDAO.delete(self._models)

def validate(self) -> None:
# Validate/populate model exists
Expand Down
12 changes: 4 additions & 8 deletions superset/commands/annotation_layer/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from functools import partial
from typing import Any, Optional

from flask_appbuilder.models.sqla import Model
Expand All @@ -28,8 +29,8 @@
)
from superset.commands.base import BaseCommand
from superset.daos.annotation_layer import AnnotationLayerDAO
from superset.daos.exceptions import DAOUpdateFailedError
from superset.models.annotations import AnnotationLayer
from superset.utils.decorators import on_error, transaction

logger = logging.getLogger(__name__)

Expand All @@ -40,16 +41,11 @@ def __init__(self, model_id: int, data: dict[str, Any]):
self._properties = data.copy()
self._model: Optional[AnnotationLayer] = None

@transaction(on_error=partial(on_error, reraise=AnnotationLayerUpdateFailedError))
def run(self) -> Model:
self.validate()
assert self._model

try:
annotation_layer = AnnotationLayerDAO.update(self._model, self._properties)
except DAOUpdateFailedError as ex:
logger.exception(ex.exception)
raise AnnotationLayerUpdateFailedError() from ex
return annotation_layer
return AnnotationLayerDAO.update(self._model, self._properties)

def validate(self) -> None:
exceptions: list[ValidationError] = []
Expand Down
Loading

0 comments on commit e148327

Please sign in to comment.