Search code examples
pythonopenapiblacksheep

How to use @doc in BlackSheep API


Use blacksheep create with the following options to create an example API:

✨ Project name: soquestion
🚀 Project template: api
🤖 Use controllers? Yes
📜 Use OpenAPI Documentation? Yes
🔧 Library to read settings essentials-configuration
🔩 App settings format YAML

This will generate a simple API based on BlackSheep, with the endpoints defined in app/controllers/examples.py:

"""
Example API implemented using a controller.
"""
from typing import List, Optional

from blacksheep.server.controllers import Controller, get, post


class ExamplesController(Controller):
    @classmethod
    def route(cls) -> Optional[str]:
        return "/api/examples"

    @classmethod
    def class_name(cls) -> str:
        return "Examples"

    @get()
    async def get_examples(self) -> List[str]:
        """
        Gets a list of examples.
        

        Lorem Ipsum Dolor Sit amet
        """
        return list(f"example {i}" for i in range(3))

    @post()
    async def add_example(self, example: str):
        """
        Adds an example.
        """

When you start the API (don't forget to create and activate a virtual environment before you do the pip install ...) with python dev.py and navigate to http://localhost:44777/docs you can see the OpenAPI documentation.

According to the documentation you can use the docstring to specify the endpoint description.

Is it somehow possible to also add documentation for the responses?

According to the documentation you can use the @docs decorator, but that only works in a simple file where @docs is defined beforehand. In the generated API @docs is defined in app/docs/__init.py__, but I can't find a way to use this inside the example.py.

The generated app/docs/__init.py__ looks like this:

"""
This module contains OpenAPI Documentation definition for the API.

It exposes a docs object that can be used to decorate request handlers with additional
information, used to generate OpenAPI documentation.
"""
from blacksheep import Application
from blacksheep.server.openapi.v3 import OpenAPIHandler
from openapidocs.v3 import Info

from app.docs.binders import set_binders_docs
from app.settings import Settings


def configure_docs(app: Application, settings: Settings):
    docs = OpenAPIHandler(
        info=Info(title=settings.info.title, version=settings.info.version),
        anonymous_access=True,
    )

    # include only endpoints whose path starts with "/api/"
    docs.include = lambda path, _: path.startswith("/api/")

    set_binders_docs(docs)

    docs.bind_app(app)

Solution

  • Given how documentation handler is defined in BlackSheep example, you cannot easily use that particular instance. The reason being docs is local to configure_docs function, therefore it cannot be used outside of it. However, that documentation decorator is simply an instance of OpenAPIHandler class, so you can move its definition outside of that function, and use it throughout your project freely. Here's a "patch" for the example project to show the naïve approach (sorry, but SO doesn't support patch/diff syntax highlight) with docs renamed to docc_handler to better distinguish it from app.docs module:

    diff --git a/app/controllers/examples.py b/app/controllers/examples.py
    index 4bb984d..41989e5 100644
    --- a/app/controllers/examples.py
    +++ b/app/controllers/examples.py
    @@ -5,6 +5,8 @@ from typing import List, Optional
     
     from blacksheep.server.controllers import Controller, get, post
     
    +from app.docs import docs_handler
    +
     
     class ExamplesController(Controller):
         @classmethod
    @@ -15,6 +17,7 @@ class ExamplesController(Controller):
         def class_name(cls) -> str:
             return "Examples"
     
    +    @docs_handler(responses={200: "OK response", 404: "No example found"})
         @get()
         async def get_examples(self) -> List[str]:
             """
    diff --git a/app/docs/__init__.py b/app/docs/__init__.py
    index 0b3d0f1..f478864 100644
    --- a/app/docs/__init__.py
    +++ b/app/docs/__init__.py
    @@ -9,18 +9,21 @@ from blacksheep.server.openapi.v3 import OpenAPIHandler
     from openapidocs.v3 import Info
     
     from app.docs.binders import set_binders_docs
    -from app.settings import Settings
    +from app.settings import load_settings, Settings
     
     
    +settings = load_settings()
    +
    +docs_handler = OpenAPIHandler(
    +    info=Info(title=settings.info.title, version=settings.info.version),
    +    anonymous_access=True,
    +)
    +
     def configure_docs(app: Application, settings: Settings):
    -    docs = OpenAPIHandler(
    -        info=Info(title=settings.info.title, version=settings.info.version),
    -        anonymous_access=True,
    -    )
     
         # include only endpoints whose path starts with "/api/"
    -    docs.include = lambda path, _: path.startswith("/api/")
    +    docs_handler.include = lambda path, _: path.startswith("/api/")
     
    -    set_binders_docs(docs)
    +    set_binders_docs(docs_handler)
     
    -    docs.bind_app(app)
    +    docs_handler.bind_app(app)
    

    I am not particularly happy about project structure here, to be honest, I even thought about moving that handler instance to a separate module and making it a singleton class. But it should get you going, and you can adapt it to you real use-case as you see fit.