Reputation: 4875
I'm trying to create a simple pluggable FastAPI application where plugins can add, or not, API endpoints
This is my folder structure:
server.py
import importlib
import pkgutil
from pathlib import Path
import uvicorn
from fastapi import FastAPI
PLUGINS_PATH = Path(__file__).parent.joinpath("plugins")
app = FastAPI()
def import_module(module_name):
"""Imports a module by it's name from plugins folder."""
module = f"plugins.{module_name}"
return importlib.import_module(module, ".")
def load_plugins() -> list:
"""Import plugins from plugins folder."""
loaded_apps = []
for _, application, _ in pkgutil.iter_modules([str(PLUGINS_PATH)]):
module = import_module(application)
print(
f"Loaded app: {module.__meta__['plugin_name']} -- version: {module.__meta__['version']}"
)
loaded_apps.append(module)
return loaded_apps
@app.get("/")
def main():
return "Hello World!"
if __name__ == "__main__":
plugins = load_plugins()
for plugin in plugins:
"""Register the plugins router."""
if "router" in plugin.__dir__():
app_router = plugin.router
app.include_router(app_router)
uvicorn.run("server:app", host="localhost", port=8000, reload=True)
And in my plugins folder I have:
The plugins/non_api_plugin/__init__.py
:
__meta__ = {"plugin_name": "NON API plugin", "version": "0.0.1"}
The plugins/<v1|v2>/__init__.py
from .routes import routes as router
__meta__ = {"plugin_name": "API <v1|v2>", "version": "0.0.1"}
And routes.py files:
from fastapi import APIRouter
routes = APIRouter(prefix="/<v1|v2>")
@routes.get("/")
def novels():
return "Hello World from <v1|v2>"
When I run the server, the plugins are loaded and log their information, but the API endpoints are not loaded.
What I'm missing here? My best guess is that my plugin load system is wrong at some point.
Upvotes: 3
Views: 4137
Reputation: 52902
You're running your plugin registration code inside the code path that only runs when your script has the main context:
if __name__ == "__main__":
# plugin registration
Since you use this section to invoke uvicorn
, uvicorn starts by itself and imports the module you're giving it. When uvicorn starts up, it imports your application and determines which endpoints are available - but now your own script is no longer the main context for the application, so anything inside the __name__ == "__main__"
block will not run.
You'll see your expected behaviour as soon as you move the plugin registration block out of that scope:
plugins = load_plugins()
for plugin in plugins:
"""Register the plugins router."""
if "router" in plugin.__dir__():
app_router = plugin.router
app.include_router(app_router, prefix='/foo') # I'd let the plugin name be the prefix here to avoid plugins using the same prefix
if __name__ == "__main__":
uvicorn.run("server:app", host="localhost", port=8000, reload=True)
Upvotes: 5