|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -from collections.abc import Iterable |
| 3 | +from collections.abc import AsyncIterator, Iterable, Mapping |
| 4 | +from contextlib import asynccontextmanager |
4 | 5 | from types import ModuleType |
| 6 | +from typing import Any, cast |
5 | 7 |
|
| 8 | +import starlette |
6 | 9 | from starlette.applications import Starlette # pylint: disable=E0401 |
| 10 | +from starlette.routing import _DefaultLifespan as StarletteDefaultLifespan |
| 11 | +from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send |
7 | 12 |
|
8 | 13 | from tortoise import Tortoise |
| 14 | +from tortoise.config import TortoiseConfig |
9 | 15 | from tortoise.connection import get_connections |
| 16 | +from tortoise.context import TortoiseContext, get_current_context |
10 | 17 | from tortoise.log import logger |
11 | 18 |
|
| 19 | +_TORTOISE_CONTEXT_STATE = "_tortoise_context" |
| 20 | + |
| 21 | + |
| 22 | +class TortoiseContextMiddleware: |
| 23 | + def __init__(self, app: ASGIApp, config: TortoiseConfig, starlette_app: Starlette) -> None: |
| 24 | + self.app = app |
| 25 | + self.config = config |
| 26 | + self.starlette_app = starlette_app |
| 27 | + |
| 28 | + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: |
| 29 | + if scope["type"] not in {"http", "websocket"}: |
| 30 | + await self.app(scope, receive, send) |
| 31 | + return |
| 32 | + |
| 33 | + app_context = getattr(self.starlette_app.state, _TORTOISE_CONTEXT_STATE, None) |
| 34 | + if app_context is None: |
| 35 | + async with TortoiseContext() as request_context: |
| 36 | + await request_context.init(self.config) |
| 37 | + await self.app(scope, receive, send) |
| 38 | + return |
| 39 | + |
| 40 | + if get_current_context() is not app_context: |
| 41 | + with app_context: |
| 42 | + await self.app(scope, receive, send) |
| 43 | + return |
| 44 | + |
| 45 | + await self.app(scope, receive, send) |
| 46 | + |
12 | 47 |
|
13 | 48 | def register_tortoise( |
14 | 49 | app: Starlette, |
15 | | - config: dict | None = None, |
| 50 | + config: dict[str, Any] | TortoiseConfig | None = None, |
16 | 51 | config_file: str | None = None, |
17 | 52 | db_url: str | None = None, |
18 | 53 | modules: dict[str, Iterable[str | ModuleType]] | None = None, |
@@ -79,16 +114,45 @@ def register_tortoise( |
79 | 114 | ConfigurationError |
80 | 115 | For any configuration error |
81 | 116 | """ |
| 117 | + typed_config = TortoiseConfig.resolve_args(config, config_file, db_url, modules) |
82 | 118 |
|
83 | | - @app.on_event("startup") |
84 | 119 | async def init_orm() -> None: # pylint: disable=W0612 |
85 | | - await Tortoise.init(config=config, config_file=config_file, db_url=db_url, modules=modules) |
| 120 | + ctx = await Tortoise.init(config=typed_config, _enable_global_fallback=True) |
| 121 | + setattr(app.state, _TORTOISE_CONTEXT_STATE, ctx) |
86 | 122 | logger.info("Tortoise-ORM started, %s, %s", get_connections()._get_storage(), Tortoise.apps) |
87 | 123 | if generate_schemas: |
88 | 124 | logger.info("Tortoise-ORM generating schema") |
89 | 125 | await Tortoise.generate_schemas() |
90 | 126 |
|
91 | | - @app.on_event("shutdown") |
92 | 127 | async def close_orm() -> None: # pylint: disable=W0612 |
93 | 128 | await Tortoise.close_connections() |
| 129 | + if hasattr(app.state, _TORTOISE_CONTEXT_STATE): |
| 130 | + delattr(app.state, _TORTOISE_CONTEXT_STATE) |
94 | 131 | logger.info("Tortoise-ORM shutdown") |
| 132 | + |
| 133 | + if starlette.__version__ < "1": |
| 134 | + if (on_event := getattr(app, "on_event", None)) is not None: |
| 135 | + on_event("startup")(init_orm) |
| 136 | + on_event("shutdown")(close_orm) |
| 137 | + return |
| 138 | + |
| 139 | + original_lifespan = app.router.lifespan_context |
| 140 | + |
| 141 | + if generate_schemas or not isinstance(original_lifespan, StarletteDefaultLifespan): |
| 142 | + |
| 143 | + @asynccontextmanager |
| 144 | + async def orm_inited_lifespan(app_: Starlette) -> AsyncIterator[Mapping[str, Any] | None]: |
| 145 | + await init_orm() |
| 146 | + try: |
| 147 | + async with original_lifespan(app_) as maybe_state: |
| 148 | + yield maybe_state |
| 149 | + finally: |
| 150 | + await close_orm() |
| 151 | + |
| 152 | + app.router.lifespan_context = cast("Lifespan[Any]", orm_inited_lifespan) |
| 153 | + |
| 154 | + if not any( |
| 155 | + middleware.cls is TortoiseContextMiddleware # type:ignore[comparison-overlap] |
| 156 | + for middleware in app.user_middleware |
| 157 | + ): |
| 158 | + app.add_middleware(TortoiseContextMiddleware, config=typed_config, starlette_app=app) |
0 commit comments