Neoteroi / BlackSheep

Fast ASGI web framework for Python
https://www.neoteroi.dev/blacksheep/
MIT License
1.86k stars 78 forks source link

Access to main app services in sub apps #228

Closed python-programmer closed 2 years ago

python-programmer commented 2 years ago

Suppose we have a system that has multiple sub apps

Main App __ User App

__ Polls App

If we register the database connection service in main app, we can access to it in sub apps (User App, Polls App)

If this feature is available, please tell us how to use it

Thank you

RobertoPrevato commented 2 years ago

Hi @python-programmer I didn't have time to reply earlier today because I was at work. 😊 Absolutely, it is possible to share the same Container of services among several instances of applications.

For example:

from blacksheep import Application

class Foo:
    def __init__(self) -> None:
        self.foo = "foo"

app = Application()

app.services.add_scoped(Foo)

@app.router.get("/")
def a_home(foo: Foo):
    return f"Hello, from Application A {foo.foo}"

child_app = Application(services=app.services)  # <---- 🌴

@child_app.router.get("/")
def b_home(foo: Foo):
    return f"Hello, from Application B {foo.foo}"

# Note: when mounting another BlackSheep application,
# make sure to handle the start and stop events of the mounted app

@app.on_start
async def handle_app_a_start(_):
    await child_app.start()

@app.on_stop
async def handle_app_a_stop(_):
    await child_app.stop()

app.mount("/second-app", child_app)

# The following lines are not necessary but are useful to debug with Visual Studio Code or PyCharm...
if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="127.0.0.1", port=44567, log_level="debug")

Note that, if you like to separate the logic that configures services for your application from the front-end layer, you can configure the rodi.Container in dedicated modules, then use it in all your applications.

python-programmer commented 2 years ago

Thank you @RobertoPrevato

I used the same aproach

def configure_services(app: Application, app_list: List[str], service_list):
    for i_app in app_list:
        app, prefix = data extraction codes from string ...
        app.mount('per app prefix', app, services=service_list)

My goal is: I want to develop a pluggable-or-modular (in this case package) system

Product: _ User App

_ Polls App

but this approach has a downside when using openapi: it cant generate openapi in a single route (Based on my research in the documentation).

for polls app http://your-site/polls/docs for users app http://your-site/users/docs ...

Brainstorming: finally I found that I can use the Router class

and in the main app:

from users.controllers import user_router
from polls.controllers import poll_router

router = Router()
router.routes.update(user_router.routes)

router.routes.update(poll_router.routes)

app = Application(router=router)

one important thing that I want for my goal is: router should have a prefix_url and also a tags for open api generation

without those:

router = Router()

@docs(tags=['User']) <------
@router.get('/api/users/') <----- /api/users/
async def users(handler: Handler) -> List[UserModel]:
    return await handler.get_users()

@docs(tags=['User']) <------
@router.post('/api/users/register') <----- /api/users/
async def register(handler: Handler, model: UserCreateModel) -> UserModel:
    return await handler.register(model)

If you have any suggestion, please let me know

Thanks

RobertoPrevato commented 2 years ago

@python-programmer some features are still missing for mounts to work properly with the generation of OpenAPI Documentation (OAD).

I will try to find the time this weekend to modify BlackSheep so that it's able to generate proper documentation for mounted apps. I think it would be nice to support both scenarios:

Side note: if you use controllers, they will have automatically a tag with the name of the controller's class.

RobertoPrevato commented 2 years ago

Hi @python-programmer Good news: I added support for generating OAD for mounted apps from parents apps, in 1.2.5. I will soon describe this in the documentation.

Example:

from dataclasses import dataclass

from openapidocs.v3 import Info

from blacksheep import Application
from blacksheep.server.openapi.v3 import OpenAPIHandler

parent = Application(show_error_details=True)
parent.mount_registry.auto_events = True
parent.mount_registry.handle_docs = True

docs = OpenAPIHandler(info=Info(title="Parent API", version="0.0.1"))
docs.bind_app(parent)

@dataclass
class CreateCatInput:
    name: str
    email: str
    foo: int

@dataclass
class CreateDogInput:
    name: str
    email: str
    example: int

@dataclass
class CreateParrotInput:
    name: str
    email: str

@parent.router.get("/")
def a_home():
    """Parent root."""
    return "Hello, from the parent app - for information, navigate to /docs"

@parent.router.get("/cats")
def get_cats_conflicting():
    """Conflict!"""
    return "CONFLICT"

child_1 = Application()

@child_1.router.get("/")
def get_cats():
    """Gets a list of cats."""
    return "Gets a list of cats."

@child_1.router.post("/")
def create_cat(data: CreateCatInput):
    """Creates a new cat."""
    return "Creates a new cat."

@child_1.router.delete("/{cat_id}")
def delete_cat(cat_id: str):
    """Deletes a cat by id."""
    return "Deletes a cat by id."

child_2 = Application()

@child_2.router.get("/")
def get_dogs():
    """Gets a list of dogs."""
    return "Gets a list of dogs."

@child_2.router.post("/")
def create_dog(data: CreateDogInput):
    """Creates a new dog."""
    return "Creates a new dog."

@child_2.router.delete("/{dog_id}")
def delete_dog(dog_id: str):
    """Deletes a dog by id."""
    return "Deletes a dog by id."

child_3 = Application()

@child_3.router.get("/")
def get_parrots():
    """Gets a list of parrots."""
    return "Gets a list of parrots"

@child_3.router.post("/")
def create_parrot(data: CreateParrotInput):
    """Creates a new parrot."""
    return "Creates a new parrot"

@child_3.router.delete("/{parrot_id}")
def delete_parrot(parrot_id: str):
    """Deletes a parrot by id."""
    return "Deletes a parrot by id."

parent.mount("/cats", child_1)
parent.mount("/dogs", child_2)
parent.mount("/parrots", child_3)

if __name__ == "__main__":
    import uvicorn

    uvicorn.run(parent, host="127.0.0.1", port=44567, log_level="debug")