piccolo_admin icon indicating copy to clipboard operation
piccolo_admin copied to clipboard

Try `piccolo_admin` with Starlite

Open dantownsend opened this issue 3 years ago • 15 comments

It was reported that Piccolo Admin doesn't work with the Starlite framework.

It should be possible to mount Piccolo Admin:

https://starlite-api.github.io/starlite/usage/2-route-handlers/3-asgi-route-handlers/

Tasks required:

  • [ ] Try getting Piccolo admin working with Starlite
  • [ ] If it doesn't work, try and work out why

dantownsend avatar Sep 01 '22 19:09 dantownsend

@dantownsend I already tried that because I wanted to make a Starlite asgi template for Piccolo ORM but I didn't succeed. I hope you know better how it could be done.

sinisaos avatar Sep 02 '22 07:09 sinisaos

@sinisaos Ah, cool! Thanks for giving it a go.

dantownsend avatar Sep 02 '22 07:09 dantownsend

@dantownsend I tried mounting Piccolo Admin on the Starlite app again, but again no luck. I think it would be useful as Starlite supports Piccolo ORM via a plugin (or we can use controlers and both ways work) and it would be great if Piccolo Admin could be used as an important part of the Piccolo ecosystem. I tried two things with the asgi decorator. First I tried changing the scope path as advised by @peterschutt here.

admin_app = create_admin(tables=[Task])

@asgi(path="/admin/")
async def admin(scope: Scope, receive: Receive, send: Send) -> None:
    scope["path"] = "/"
    await admin_app(scope=scope, receive=receive, send=send)

app = Starlite(route_handlers=[admin])

The admin app appears to be mounted, but all dist files and folders return a 404 and none of the admin api endpoints work.

starlite_admin

After that I tried using the Starlette router to omit the scope changes and it works, but the result is the same.

from starlette.routing import Router, Mount

admin_app = create_admin(tables=[Task])


@asgi(path="/admin/")
async def admin(scope: Scope, receive: Receive, send: Send) -> None:
    router = Router(
        [
            Mount(
                path="/admin/",
                app=admin_app,
            ),
        ]
    )
    await router(scope=scope, receive=receive, send=send)

app = Starlite(route_handlers=[admin, home])

I also tried using 2 routers but the result is always the same. The admin app appears to be mounted, but nothing from admin works. I don't know if the problem is on our side because admin uses FastAPI (or Starlite different router architecture), but that doesn't make sense because both Piccolo Admin or FastAPI app are asgi apps and should work. I appreciate any help.

sinisaos avatar Sep 13 '22 16:09 sinisaos

First I tried changing the scope path as advised by @peterschutt here.

In that case OP was trying to mount another Starlite application instance that didn't have routes registered under the url prefix that the app was being mounted under.

Not sure what the issue is here but I'll try to have a look at this one today.

peterschutt avatar Sep 13 '22 20:09 peterschutt

@peterschutt Thank you very much for your reply. If you find time, it would be great if you try to integrate Piccolo Admin and Starlite. For now, Piccolo Admin is working with Starlette, FastAPI, BlackSheep and Xpresso, but all these frameworks have the ability to mount another asgi app on the application instance, which is not the case in Starlite. The only way seems to be the one I tried, but unfortunately it doesn't work as we would like.

sinisaos avatar Sep 14 '22 06:09 sinisaos

@sinisaos Do you have a demo Starlite project on GitHub? If so I'll download it and have a go.

dantownsend avatar Sep 14 '22 21:09 dantownsend

@dantownsend this is what I've been experimenting with: https://github.com/peterschutt/starlite-hello-world/tree/piccolo-admin

There's some instruction in app/main.py to get it running.

Running the starlite app, it will serve the root page, but all the static links (and probably api links) that come from the static apps and private/public apps that are mounted inside the admin app would need to have the /admin prefix to get routed back through the @asgi() decorated handler via starlite routing again. If we get it to this point, we'll need to add an /admin/{path:path} route to field the child routes as well.

image

E.g., in that example the js/... routes don't exist as far as starlite's router is concerned.

I was going to try to experiment with passing root_path to the fastapi app constructor via AdminRouter but hit that issue with the editable installs.

peterschutt avatar Sep 14 '22 21:09 peterschutt

@dantownsend No. I just changed the Starlette template to work with Starlite. You can download the zip file and pip install Starlite (forgot to add in requirements.txt). Use it like any Piccolo asgi template (running migrations etc.).

tasks.zip

sinisaos avatar Sep 15 '22 04:09 sinisaos

I've looked into it - comparing how Starlite works vs Starlette.

I mounted Piccolo Admin in each, and looked at the ASGI scope.

The root_path and path values are opposite.

Here's Starlite:

starlite_scope = {
    'type': 'http',
    'asgi': {
        'version': '3.0',
        'spec_version': '2.3'
    },
    'http_version': '1.1',
    'server': ('127.0.0.1', 8000),
    'client': ('127.0.0.1', 65236),
    'scheme': 'http',
    'method': 'GET',
    'root_path': '',      # NOTE
    'path': '/admin/',   # NOTE
    'raw_path': b'/admin/',
    'query_string': b'',
    ...
}

Here's Starlette:

starlette_scope = {
    'type': 'http',
    'asgi': {
        'version': '3.0',
        'spec_version': '2.3'
    },
    'http_version': '1.1',
    'server': ('127.0.0.1', 8000),
    'client': ('127.0.0.1', 64073),
    'scheme': 'http',
    'method': 'GET',
    'root_path': '/admin',     # NOTE
    'path': '/',                         # NOTE
    'raw_path': b'/admin/',
    'query_string': b'',
    ...
}

I used a setup similar to @sinisaos for the testing:

admin_app = create_admin(tables=[Task])

@asgi(path="/admin/")
async def admin(scope: Scope, receive: Receive, send: Send) -> None:
    await admin_app(scope=scope, receive=receive, send=send)

I don't think Starlite is forwarding all sub paths on /admin/. For example, /admin/api isn't getting sent to Piccolo Admin. I tried doing things like @asgi(path="/admin/*"), but no luck.

The best docs on how mounting works with ASGI is BlackSheep:

https://www.neoteroi.dev/blacksheep/mounting/

dantownsend avatar Sep 15 '22 23:09 dantownsend

Somehow I managed to integrate Starlite and Piccolo Admin.

https://user-images.githubusercontent.com/30960668/190585807-73195d6c-0ae1-4049-96f1-f6a194a66481.mp4

First I used the Starlette Mount

admin_app = create_admin(tables=[Task])

@asgi(path="/admin")
async def admin(scope: Scope, receive: Receive, send: Send) -> None:
    router = Router(
        [
            Mount(
                path="/admin",
                app=admin_app,
            ),
        ]
    )
    await router(scope=scope, receive=receive, send=send)

After that I changed this lines of code in Starlite asgi.py https://github.com/starlite-api/starlite/blob/a3056724a7c4ef1d83dcdf60fd4498d34c5f62f8/starlite/asgi.py#L168-L169 to this

if path != "/":
    path = f"/{scope['path'].split('/')[1]}"

to strip scope path from e.g /admin/api/user to only /admin. Of course there is a problem because that line of code strip other paths that are not the root path / e.g tasks/1 does not show only 1 record but all (at the end of video).

It just a start and it's not great but that shows that Piccolo Admin can be mount to Starlite.

sinisaos avatar Sep 16 '22 08:09 sinisaos

@sinisaos Hmm, interesting.

dantownsend avatar Sep 16 '22 08:09 dantownsend

It's not an elegant solution, but maybe as @peterschutt a Starlite maintainer knows better how to fix the problem. Basically, in Starlite we should detect when an admin application is mounted and strip only that scope path, leaving other paths (e.g. tasks/1) untouched. Or something else, feel free to suggest what else could be done. Thanks in advance.

sinisaos avatar Sep 16 '22 08:09 sinisaos

Great stuff @sinisaos - I'll have a closer look when I get a chance and work out a plan of attack. I think the key thing that the starlette router is doing in your mvce is that it manages stripping the mount prefix from scope["path"] and setting it to scope["root_path"] before forwarding on to the underlying app. I believe the necessity to strip the remaining path after the route prefix is due to the app being mounted on @asgi("/admin") which doesn't match any sub-routes of that path. So would also prob need a mount at @asgi("/admin/{path:path}") - but that's obviously not what we'd want to force users to do.

I think we can prob support a mount() method on Starlite, and before throwing a 404 if the path drops through our routing, check if the prefix matches a mounted path, and then pipe the request through the mounted app. However, take this all with a grain of salt until I get a chance to play around with some implementations, devil is always in the detail:)

peterschutt avatar Sep 16 '22 09:09 peterschutt

@peterschutt Thanks. It would be great if you try to find a better solution (the important thing is that now we have at least some solution that shows that Piccolo Admin can be mounted on the Starlite app). mount() would be the best solution if it suits the Starlite router implementation and the community. BlackSheep mount() implementation would be the best example (here and here and here is Piccolo admin usage example). If we end up with a good solution, we could make a Starlite asgi starter template using the crud controller (same as for Starlette, BlackSheep, etc.) and I think that would also benefit Starlite (which already supports Piccolo ORM through a plugin) because Piccolo Admin is an important part of Piccolo ecosystem.

sinisaos avatar Sep 16 '22 11:09 sinisaos

@peterschutt I have some update on how we can use Piccolo Admin mount a little better if mount() is not an option or it's difficult to implement, without too much intervention in the Starlite code. Route handlers have a name argument where we can pass e.g. Piccolo Admin (or any other name you think is fine). We can then use that name so Starlite can recognize it as a handler for Piccolo Admin without affecting any other routes (every other route with or without path or query params work as expected). To do this we only need to add these few lines of code, which I don't think will affect coverage even if you don't have test for that. Changes to asgi.py

def _parse_scope_to_route(
        self, scope: "Scope"
    ) -> Tuple[Dict[str, "ASGIApp"], bool]:
        """Given a scope object, retrieve the _asgi_handlers and _is_asgi
        values from correct trie node."""

        path = cast("str", scope["path"]).strip()
        # used only for mounted Piccolo Admin app
        try:
            piccolo_admin_path = self.app._route_handler_index["Piccolo Admin"]["path"] # pylint: disable=W0212
            if piccolo_admin_path in path:
                path = piccolo_admin_path
        except KeyError:
            pass
        # the rest of the code is unchanged
        if path != "/" and path.endswith("/"):
            path = path.rstrip("/")
        if path in self.app.plain_routes:
            current_node: RouteMapNode = self.app.route_map[path]
            path_params: List[str] = []
        else:
            current_node, path_params = self._traverse_route_map(
                path=path, scope=scope
            )
         ...

Usage with changes in app.py

# mount Piccolo Admin app
@asgi(path="/admin", name="Piccolo Admin")
async def admin(scope: Scope, receive: Receive, send: Send) -> None:
    router = Router(
        [
            Mount(
                path="/admin",
                app=create_admin(tables=[Task]),
            ),
        ]
    )
    await router(scope=scope, receive=receive, send=send)

sinisaos avatar Sep 17 '22 17:09 sinisaos

@peterschutt Wildcard URL matching did the trick to mount Piccolo Admin in the Starlite app without modifying the Starlite source code. Mystery solved.

from starlette.routing import Mount, Router

# mount Piccolo Admin app
@asgi(path="/{admin:path}") # <- Wildcard URL matching
async def admin(scope: Scope, receive: Receive, send: Send) -> None:
    router = Router(
        [
            Mount(
                path="/admin",
                app=create_admin(tables=[Task]),
            ),
        ]
    )
    await router(scope=scope, receive=receive, send=send)

@dantownsend In the next few days I will do a PR to Piccolo ORM to add the Starlite asgi template.

sinisaos avatar Sep 23 '22 19:09 sinisaos

Great stuff, @sinisaos.

I still think Starlite should have a better way to mount an asgi app such as create_admin without all the extra stuff you've had to do here such as using starlette's Mount and Router. I just haven't found the time to dedicate to it yet.

peterschutt avatar Sep 23 '22 20:09 peterschutt

@peterschutt Thanks for your effort and help. I will create a Starlite asgi template in case someone wants to use Starlite with the full Piccolo ecosystem (ORM + admin) now. If you come up with a better way to mount admin, we can always make changes.

sinisaos avatar Sep 23 '22 20:09 sinisaos

@peterschutt Here is the Starlite asgi starter template. Thanks again for your help. If you come up with a better mounting solution, feel free to make PR to change it. @dantownsend You can close this issue as it is done.

sinisaos avatar Sep 25 '22 08:09 sinisaos

Thanks for helping to grow the Starlite community @sinisaos!

peterschutt avatar Sep 26 '22 07:09 peterschutt

@dantownsend You can close this because it's done.

sinisaos avatar Sep 30 '22 14:09 sinisaos

Just an update - starlite v1.35.0 had proper support for mounts now. The @asgi decorator receives an optional kwarg is_mount that enables this.

Goldziher avatar Oct 29 '22 18:10 Goldziher

@Goldziher Great, thanks for the update.

We'll update Piccolo's Starlite template to use this new feature in a future version.

dantownsend avatar Oct 29 '22 19:10 dantownsend

@dantownsend I tried to use this new feature but failed to mount admin. I tried

@asgi("/admin", is_mount=True)
async def admin(scope: "Scope", receive: "Receive", send: "Send") -> None:
    await create_admin(tables=APP_CONFIG.table_classes)(scope, receive, send)

and

admin = asgi(path="/admin", is_mount=True)(
    create_admin(tables=APP_CONFIG.table_classes)
)

but it didn't work. I'm probably doing something wrong, so it would be great if someone from the Starlite community could show how to use this new feature. There is also a missing cryptography dependency when installing Starlite.

sinisaos avatar Oct 30 '22 08:10 sinisaos

@sinisaos Thanks for trying it out.

dantownsend avatar Oct 30 '22 08:10 dantownsend

@sinisaos you wouldn't happen to have a repro handy would you?

peterschutt avatar Oct 30 '22 10:10 peterschutt

@peterschutt I'm sorry but I don't understand. Do you think I have a github repo with the code? I have not. I just tried to change the admin mount function from the Starlite template (generated from piccolo asgi new) with the new code from this example and this, but it doesn't work. If you know how to make it work that would be great.

sinisaos avatar Oct 30 '22 14:10 sinisaos

@peterschutt I'm sorry but I don't understand. Do you think I have a github repo with the code? I have not. I just tried to change the admin mount function from the Starlite template (generated from piccolo asgi new) with the new code from this example and this, but it doesn't work. If you know how to make it work that would be great.

The starlite template you linked was exactly what I'm looking for, cheers.

peterschutt avatar Oct 30 '22 14:10 peterschutt

The problem might be incompatible Starlette versions. Piccolo Admin has FastAPI as a dependency, which only works with older versions of Starlette.

dantownsend avatar Oct 30 '22 14:10 dantownsend

Thanks @dantownsend - I've opened a tracking issue on our side, we'll get to the bottom of it (hopefully!) :+1:

peterschutt avatar Oct 30 '22 14:10 peterschutt