-
Notifications
You must be signed in to change notification settings - Fork 0
FastAPI Integration
PluginForge can mount plugin routes onto a FastAPI application. FastAPI is an optional dependency: install it separately when you need this integration.
pip install pluginforge fastapiNote: the pluginforge[fastapi] extra existed pre-v0.6.0 but was non-functional (it referenced a dependency that was never declared as optional). It was removed in v0.6.0.
from fastapi import FastAPI
from pluginforge import PluginManager
app = FastAPI()
pm = PluginManager("config/app.yaml")
pm.discover_plugins()
pm.mount_routes(app)The default prefix is /api. You can change it:
pm.mount_routes(app, prefix="/v2/api")mount_routes is idempotent: calling it twice with the same plugin set on the same FastAPI app is a no-op. Long pytest sweeps that re-enter FastAPI lifespans on a shared app no longer accumulate route registrations. Tracking state lives in a module-level WeakKeyDictionary[FastAPI, set[str]] inside pluginforge.fastapi_ext (per-app, weakref-keyed so the entry is released when the app is garbage-collected).
This fix closes a recursion-cascade Bibliogon reported (RecursionError in starlette.testclient.TestClient.wait_startup after long sweeps). pluginforge.testing.IsolatedPluginManager clears the tracking on context entry and exit; use it as the canonical test fixture for consumer-app suites that mount routes across many TestClient lifespans.
Plugins provide routes by overriding get_routes(). Plugins bring their own route structure via their routers:
from fastapi import APIRouter
from pluginforge import BasePlugin
class HelloPlugin(BasePlugin):
name = "hello"
def get_routes(self) -> list:
router = APIRouter(prefix="/hello")
@router.get("/greet")
def greet():
greeting = self.config.get("greeting", "Hello")
return {"message": greeting}
@router.get("/info")
def info():
return {"plugin": self.name, "version": self.version}
return [router]With default prefix, routes are available at:
GET /api/hello/greet -> HelloPlugin.greet()
GET /api/hello/info -> HelloPlugin.info()
Plugins control their own URL structure. An export plugin can define routes like /books/{book_id}/export/{format} and they will be mounted under the prefix:
GET /api/books/{book_id}/export/pdf
get_routes() should return exactly one APIRouter. Nest namespace-separated sub-routers via router.include_router(sub_router).
# Recommended: one parent router with nested sub-routers.
panels_router = APIRouter(prefix="/panels")
panels_router.add_api_route("/", list_panels, methods=["GET"])
bubbles_router = APIRouter(prefix="/bubbles")
bubbles_router.add_api_route("/", list_bubbles, methods=["GET"])
class ComicsPlugin(BasePlugin):
name = "comics"
def get_routes(self) -> list:
parent = APIRouter(prefix="/comics")
parent.include_router(panels_router)
parent.include_router(bubbles_router)
return [parent]Plugins returning more than one top-level router from get_routes() emit a DeprecationWarning since v0.8.0. The warning continues in v0.9.0; v0.10.0 may make it an error. See docs/guides/plugin-author.md for the full rationale and migration recipe.
You can expose a health endpoint using pm.health_check():
@app.get("/api/health")
def health():
return pm.health_check()Response:
{
"export": {"status": "ok", "formats": ["epub", "pdf"]},
"grammar": {"status": "error", "error": "LanguageTool API unreachable"}
}- If
fastapiis not installed,mount_routes()raisesImportErrorwith installation instructions - If the
appargument is not a FastAPI instance, raisesTypeError - Plugins that return empty routes from
get_routes()are silently skipped