Search code examples
fastapiopenapiopenapi-generatorswagger-codegenuvicorn

How to implement custom logic in OpenAPI-generated FastAPI project?


I'm working with a FastAPI project generated by OpenAPI Generator. I want to implement custom logic for my API endpoints, but I'm unsure how to ensure my changes are picked up when running the server.

Project Structure

media_api_python/
└── openapi_server/
    ├── apis/
    │   ├── __init__.py
    │   ├── media_files_api.py
    │   └── media_files_api_base.py
    ├── impl/
    │   ├── __init__.py
    │   └── media_files_api.py
    ├── main.py
    └── models/
        ├── __init__.py
        ├── appearance.py
        ├── bounding_box.py
        ...

Question

  1. How can I implement custom logic in impl/media_files_api.py and ensure it's used when running the server with uvicorn openapi_server.main:app --host 0.0.0.0 --port 8080?

  2. Can you provide a simple "Hello World" implementation of the media_files_mediafile_post function to demonstrate this approach?

Relevant Code

Here's the content of media_files_api_base.py:

class BaseMediaFilesApi:
    subclasses: ClassVar[Tuple] = ()

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        BaseMediaFilesApi.subclasses = BaseMediaFilesApi.subclasses + (cls,)

    def media_files_mediafile_post(self, file: str) -> object:
        ...

And here's the relevant part of media_files_api.py:

router = APIRouter()

ns_pkg = openapi_server.impl
for _, name, _ in pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + "."):
    importlib.import_module(name)

@router.post("/MediaFiles/mediafile", ...)
async def media_files_mediafile_post(file: str = Form(None, description="")) -> object:
    ...

I'd appreciate any guidance on how to properly implement and integrate custom logic in this OpenAPI-generated FastAPI project. Thanks in advance!


In my attempt at solving it, I created this file impl/media_files_api.py with the following code, with the expectation that it would somehow be picked up and would override the corresponding endpoints in apis/media_files_api.py, but when I run curl -X POST "http://localhost:8080/MediaFiles/mediafile" -H "Content-Type: application/x-www-form-urlencoded" --data-urlencode "file=$(< setup.cfg)", it returns null, instead of {"Hello": "World: setup.cfg"}

from openapi_server.apis.media_files_api_base import *

class MediaFilesApiImpl(BaseMediaFilesApi):
    def media_files_mediafile_post(self, file: str):
        print({"Hello": f"World: {file}"}, file)
        return {"Hello": f"World: {file}"}

Solution

  • I figured out that the default OpenAPI-provided mustache template used to generate the API file, media_files_api.py, has a bug as it fails to generate the return statements for all the endpoints. For the endpoint in the question, the return statement looks like this:

       return BaseMediaFilesApi.subclasses[0]().media_files_mediafile_post(file)
    

    To fix this, I had to modify the api.mustache file by substituting L67-L68,

        {{#notes}}"""{{.}}"""
        return Base{{classname}}.subclasses[0]().{{operationId}}({{#allParams}}{{>impl_argument}}{{^-last}}, {{/-last}}{{/allParams}}){{/notes}}{{^notes}}...{{/notes}}
    

    with

        return Base{{classname}}.subclasses[0]().{{operationId}}({{#allParams}}{{>impl_argument}}{{^-last}}, {{/-last}}{{/allParams}})
    

    Bonus: I needed to fix many issues relating to non-primitive model types and a few other customizations without having to directly modify any of the generated files. I found modifying the mustache template the easiest way to achieve that. Additionally, do watch out for template composition –– thus, what you need to change might, for example, be a subfile that is inserted into the api.mustache file.