Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .schemas/plugin.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@
"enum": ["mysql", "postgresql"]
},
"x-tombi-array-values-order": "ascending"
},
"depends_on": {
"type": "array",
"description": "List of plugin names this plugin depends on (used for startup order)",
"items": {
"type": "string",
"minLength": 1
},
"x-tombi-array-values-order": "ascending"
}
}
},
Expand Down
Empty file.
10 changes: 10 additions & 0 deletions backend/common/dataclasses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dataclasses

from datetime import datetime
from typing import Any

from fastapi import Response

Expand Down Expand Up @@ -73,3 +74,12 @@ class SnowflakeInfo:
datacenter_id: int
worker_id: int
sequence: int


@dataclasses.dataclass(slots=True)
class PluginEntry:
name: str
depends_on: list[str] | None = None
extend: str | None = None
routers: list[str] | None = None
api: dict[str, Any] | None = None
8 changes: 8 additions & 0 deletions backend/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,11 @@ class PrimaryKeyType(StrEnum):

autoincrement = 'autoincrement'
snowflake = 'snowflake'


class LifespanStage(IntEnum):
"""lifespan 执行阶段"""

core = 0
plugin = 1
tail = 2
49 changes: 38 additions & 11 deletions backend/common/lifespan.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,54 @@
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
from typing import Any
from typing import Any, overload

from fastapi import FastAPI

from backend.common.enums import LifespanStage

LifespanFunc = Callable[[FastAPI], AbstractAsyncContextManager[dict[str, Any] | None]]


class LifespanManager:
"""FastAPI lifespan 管理器"""

def __init__(self) -> None:
self._lifespans: list[LifespanFunc] = []
self._lifespans: dict[LifespanStage, list[LifespanFunc]] = {
LifespanStage.core: [],
LifespanStage.plugin: [],
LifespanStage.tail: [],
}

@overload
def register(self, func: LifespanFunc) -> LifespanFunc: ...

def register(self, func: LifespanFunc) -> LifespanFunc:
@overload
def register(self, *, stage: LifespanStage) -> Callable[[LifespanFunc], LifespanFunc]: ...

def register(
self, func: LifespanFunc | None = None, *, stage: LifespanStage = LifespanStage.core
) -> LifespanFunc | Callable[[LifespanFunc], LifespanFunc]:
"""
注册 lifespan hook

:param func: lifespan hook
:param func: lifespan hook(直接装饰时使用)
:param stage: 执行阶段,控制粗粒度顺序,默认为 core
:return:
"""
if func not in self._lifespans:
self._lifespans.append(func)
return func

def decorator(f: LifespanFunc) -> LifespanFunc:
for hooks in self._lifespans.values():
for fn in hooks:
if fn is f:
return f

self._lifespans[stage].append(f)
return f

if func is not None:
return decorator(func)

return decorator

def build(self) -> LifespanFunc:
"""
Expand All @@ -35,10 +61,11 @@ def build(self) -> LifespanFunc:
async def combined_lifespan(app: FastAPI): # noqa: ANN202
state: dict[str, Any] = {}
async with AsyncExitStack() as exit_stack:
for lifespan_fn in self._lifespans:
result = await exit_stack.enter_async_context(lifespan_fn(app))
if isinstance(result, dict):
state.update(result)
for stage in LifespanStage:
for lifespan_fn in self._lifespans[stage]:
result = await exit_stack.enter_async_context(lifespan_fn(app))
if isinstance(result, dict):
state.update(result)

for key, value in state.items():
setattr(app.state, key, value)
Expand Down
Loading