Skip to content

Commit

Permalink
feat(flask): Auto extend Flask CLI and add session integration (#111)
Browse files Browse the repository at this point in the history
The Advanced Alchemy alembic CLI is now auto-extended to your Flask application.

The Flask extension now also has a session handling middleware for handling auto-commits.

Last, but not least, there's an experimental async portal that integrates a long running asyncio loop for running async operations in Flask.  Using `foo = portal.call(<async function>)` you can get the result of an asynchronous function from a sync context.
  • Loading branch information
cofin authored Jan 17, 2025
1 parent b3c44cc commit 9cb3154
Show file tree
Hide file tree
Showing 23 changed files with 2,128 additions and 204 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ repos:
- id: unasyncd
additional_dependencies: ["ruff"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: "v0.9.1"
rev: "v0.9.2"
hooks:
# Run the linter.
- id: ruff
Expand Down
86 changes: 86 additions & 0 deletions advanced_alchemy/extensions/flask/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Flask extension for Advanced Alchemy.
This module provides Flask integration for Advanced Alchemy, including session management,
database migrations, and service utilities.
Example:
Basic usage with synchronous SQLAlchemy:
```python
from flask import Flask
from advanced_alchemy.extensions.flask import (
AdvancedAlchemy,
SQLAlchemySyncConfig,
EngineConfig,
)
app = Flask(__name__)
db_config = SQLAlchemySyncConfig(
engine_config=EngineConfig(url="sqlite:///db.sqlite3"),
create_all=True, # Create tables on startup
)
db = AdvancedAlchemy(config=db_config)
db.init_app(app)
# Get a session in your route
@app.route("/")
def index():
session = db.get_session()
# Use session...
```
Using async SQLAlchemy:
```python
from advanced_alchemy.extensions.flask import (
AdvancedAlchemy,
SQLAlchemyAsyncConfig,
)
app = Flask(__name__)
db_config = SQLAlchemyAsyncConfig(
engine_config=EngineConfig(
url="postgresql+asyncpg://user:pass@localhost/db"
),
create_all=True,
)
db = AdvancedAlchemy(config=db_config)
db.init_app(app)
```
"""

from advanced_alchemy import base, exceptions, filters, mixins, operations, repository, service, types, utils
from advanced_alchemy.alembic.commands import AlembicCommands
from advanced_alchemy.config import AlembicAsyncConfig, AlembicSyncConfig, AsyncSessionConfig, SyncSessionConfig
from advanced_alchemy.extensions.flask.cli import get_database_migration_plugin
from advanced_alchemy.extensions.flask.config import EngineConfig, SQLAlchemyAsyncConfig, SQLAlchemySyncConfig
from advanced_alchemy.extensions.flask.extension import AdvancedAlchemy
from advanced_alchemy.extensions.flask.service import FlaskServiceMixin

__all__ = (
"AdvancedAlchemy",
"AlembicAsyncConfig",
"AlembicCommands",
"AlembicSyncConfig",
"AsyncSessionConfig",
"EngineConfig",
"FlaskServiceMixin",
"SQLAlchemyAsyncConfig",
"SQLAlchemySyncConfig",
"SyncSessionConfig",
"base",
"exceptions",
"filters",
"get_database_migration_plugin",
"mixins",
"operations",
"repository",
"service",
"types",
"utils",
)
72 changes: 72 additions & 0 deletions advanced_alchemy/extensions/flask/alembic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Alembic integration for Flask applications."""

from __future__ import annotations

from contextlib import suppress
from typing import TYPE_CHECKING, Any, cast

from advanced_alchemy.alembic.commands import AlembicCommandConfig
from advanced_alchemy.alembic.commands import AlembicCommands as _AlembicCommands
from advanced_alchemy.exceptions import ImproperConfigurationError

if TYPE_CHECKING:
from flask import Flask

from advanced_alchemy.extensions.flask.extension import AdvancedAlchemy


def get_sqlalchemy_extension(app: Flask) -> AdvancedAlchemy:
"""Retrieve Advanced Alchemy extension from the Flask application.
Args:
app: The :class:`flask.Flask` application instance.
Returns:
:class:`AdvancedAlchemy`: The Advanced Alchemy extension instance.
Raises:
:exc:`advanced_alchemy.exceptions.ImproperConfigurationError`: If the extension is not found.
"""
with suppress(KeyError):
return cast("AdvancedAlchemy", app.extensions["advanced_alchemy"])
msg = "Failed to initialize database migrations. The Advanced Alchemy extension is not properly configured."
raise ImproperConfigurationError(msg)


class AlembicCommands(_AlembicCommands):
"""Flask-specific implementation of Alembic commands.
Args:
app: The :class:`flask.Flask` application instance.
"""

def __init__(self, app: Flask) -> None:
"""Initialize the Alembic commands.
Args:
app: The Flask application instance.
"""
self._app = app
self.db = get_sqlalchemy_extension(self._app)
self.config = self._get_alembic_command_config()

def _get_alembic_command_config(self) -> AlembicCommandConfig:
"""Get the Alembic command configuration.
Returns:
:class:`AlembicCommandConfig`: The command configuration instance.
"""
kwargs: dict[str, Any] = {}
if self.sqlalchemy_config.alembic_config.script_config:
kwargs["file_"] = self.sqlalchemy_config.alembic_config.script_config
if self.sqlalchemy_config.alembic_config.template_path:
kwargs["template_directory"] = self.sqlalchemy_config.alembic_config.template_path
kwargs.update(
{
"engine": self.sqlalchemy_config.get_engine(),
"version_table_name": self.sqlalchemy_config.alembic_config.version_table_name,
},
)
self.config = AlembicCommandConfig(**kwargs)
self.config.set_main_option("script_location", self.sqlalchemy_config.alembic_config.script_location)
return self.config
69 changes: 69 additions & 0 deletions advanced_alchemy/extensions/flask/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Command-line interface utilities for Flask integration.
This module provides CLI commands for database management in Flask applications.
"""

from __future__ import annotations

from contextlib import suppress
from typing import TYPE_CHECKING, cast

from flask import current_app

from advanced_alchemy.cli import add_migration_commands

try:
import rich_click as click
except ImportError:
import click # type: ignore[no-redef]


if TYPE_CHECKING:
from flask import Flask

from advanced_alchemy.extensions.flask.extension import AdvancedAlchemy


def get_database_migration_plugin(app: Flask) -> AdvancedAlchemy:
"""Retrieve the Advanced Alchemy extension from the Flask application.
Args:
app: The :class:`flask.Flask` application instance.
Returns:
:class:`AdvancedAlchemy`: The Advanced Alchemy extension instance.
Raises:
:exc:`advanced_alchemy.exceptions.ImproperConfigurationError`: If the extension is not found.
Example:
```python
from flask import Flask
from advanced_alchemy.extensions.flask import (
get_database_migration_plugin,
)
app = Flask(__name__)
db = get_database_migration_plugin(app)
```
"""
from advanced_alchemy.exceptions import ImproperConfigurationError

with suppress(KeyError):
return cast("AdvancedAlchemy", app.extensions["advanced_alchemy"])
msg = "Failed to initialize database migrations. The Advanced Alchemy extension is not properly configured."
raise ImproperConfigurationError(msg)


@click.group(name="database")
@click.pass_context
def database_group(ctx: click.Context) -> None:
"""Manage SQLAlchemy database components.
This command group provides database management commands like migrations.
"""
ctx.ensure_object(dict)
ctx.obj["configs"] = get_database_migration_plugin(current_app).config


add_migration_commands(database_group)
Loading

0 comments on commit 9cb3154

Please sign in to comment.