efirvida
efirvida

Reputation: 4875

FastAPI with APIRouter plugin system not working

I'm trying to create a simple pluggable FastAPI application where plugins can add, or not, API endpoints

This is my folder structure:

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

Answers (1)

MatsLindh
MatsLindh

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

Related Questions