-
-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(flask): Auto extend Flask CLI and add session integration (#111)
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
Showing
23 changed files
with
2,128 additions
and
204 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.